diff --git a/app/javascript/application.js b/app/javascript/application.js
index 6f2dbdd21..acc47a64e 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -13,6 +13,7 @@ import NativeAction from './elements/native_action'
import NativeSearch from './elements/native_search'
import NativeModal from './elements/native_modal'
import NativeEvent from './elements/native_event'
+import NativeUpload from './elements/native_upload'
import FormPrompt from './elements/form_prompt'
import InfiniteScroll from './elements/infinite_scroll'
import ModalButton from './elements/modal_button'
@@ -129,6 +130,7 @@ safeRegisterElement('native-action', NativeAction)
safeRegisterElement('native-search', NativeSearch)
safeRegisterElement('native-modal', NativeModal)
safeRegisterElement('native-event', NativeEvent)
+safeRegisterElement('native-upload', NativeUpload)
safeRegisterElement('form-prompt', FormPrompt)
safeRegisterElement('infinite-scroll', InfiniteScroll)
safeRegisterElement('modal-button', ModalButton)
diff --git a/app/javascript/elements/native_upload.js b/app/javascript/elements/native_upload.js
new file mode 100644
index 000000000..f781b8567
--- /dev/null
+++ b/app/javascript/elements/native_upload.js
@@ -0,0 +1,45 @@
+import { convertImagesInInput } from './convert_upload'
+
+export default class extends HTMLElement {
+ connectedCallback () {
+ this.files = []
+
+ document.addEventListener('turbo:submit-end', this.onSubmitEnd)
+ }
+
+ disconnectedCallback () {
+ document.removeEventListener('turbo:submit-end', this.onSubmitEnd)
+ }
+
+ get form () {
+ return this.querySelector('form')
+ }
+
+ append (index, name, type, base64) {
+ const file = this.files[index] ||= { name, type, chunks: [] }
+
+ file.chunks.push(Uint8Array.from(atob(base64), (char) => char.charCodeAt(0)))
+ }
+
+ async submit (url) {
+ const dataTransfer = new DataTransfer()
+
+ this.files.forEach(({ name, type, chunks }) => dataTransfer.items.add(new File(chunks, name, { type })))
+ this.files = []
+
+ const input = this.form.querySelector('[type="file"]')
+
+ input.files = dataTransfer.files
+
+ await convertImagesInInput(input)
+
+ this.form.action = url
+ this.form.requestSubmit()
+ }
+
+ onSubmitEnd = (e) => {
+ if (e.detail.formSubmission?.formElement !== this.form) return
+
+ window.webkit?.messageHandlers?.native?.postMessage({ type: 'upload', success: e.detail.success })
+ }
+}
diff --git a/app/views/shared/_native_navbar.html.erb b/app/views/shared/_native_navbar.html.erb
index 878315910..68933e888 100644
--- a/app/views/shared/_native_navbar.html.erb
+++ b/app/views/shared/_native_navbar.html.erb
@@ -11,6 +11,14 @@
<% end %>
+ <% if can?(:create, ::Template) %>
+
+ <%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, html: { enctype: 'multipart/form-data' } do %>
+
+
+ <% end %>
+
+ <% end %>
<% if can?(:create, ::Template) && controller_name.in?(%w[templates_dashboard submissions_dashboard template_folders templates_archived submissions_archived]) %>
<% folder_name = @template_folder&.full_name %>
">