forked from coracle/flotilla
fix: support native clipboard image paste on mobile (#181)
Co-authored-by: Khushvendra <khushvendras99@gmail.com> Co-committed-by: Khushvendra <khushvendras99@gmail.com>
This commit is contained in:
@@ -48,6 +48,7 @@
|
|||||||
"@capacitor/android": "^8.0.1",
|
"@capacitor/android": "^8.0.1",
|
||||||
"@capacitor/app": "^8.0.0",
|
"@capacitor/app": "^8.0.0",
|
||||||
"@capacitor/cli": "^8.0.1",
|
"@capacitor/cli": "^8.0.1",
|
||||||
|
"@capacitor/clipboard": "^8.0.1",
|
||||||
"@capacitor/core": "^8.0.1",
|
"@capacitor/core": "^8.0.1",
|
||||||
"@capacitor/filesystem": "^8.1.0",
|
"@capacitor/filesystem": "^8.1.0",
|
||||||
"@capacitor/ios": "^8.0.1",
|
"@capacitor/ios": "^8.0.1",
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@tiptap/core": "^2.27.2",
|
"@tiptap/core": "^2.27.2",
|
||||||
|
"@tiptap/pm": "^2.27.2",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
|
|||||||
Generated
+15
@@ -26,6 +26,9 @@ importers:
|
|||||||
'@capacitor/cli':
|
'@capacitor/cli':
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.0.1
|
version: 8.0.1
|
||||||
|
'@capacitor/clipboard':
|
||||||
|
specifier: ^8.0.1
|
||||||
|
version: 8.0.1(@capacitor/core@8.0.1)
|
||||||
'@capacitor/core':
|
'@capacitor/core':
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.0.1
|
version: 8.0.1
|
||||||
@@ -71,6 +74,9 @@ importers:
|
|||||||
'@tiptap/core':
|
'@tiptap/core':
|
||||||
specifier: ^2.27.2
|
specifier: ^2.27.2
|
||||||
version: 2.27.2(@tiptap/pm@2.27.2)
|
version: 2.27.2(@tiptap/pm@2.27.2)
|
||||||
|
'@tiptap/pm':
|
||||||
|
specifier: ^2.27.2
|
||||||
|
version: 2.27.2
|
||||||
'@types/qrcode':
|
'@types/qrcode':
|
||||||
specifier: ^1.5.6
|
specifier: ^1.5.6
|
||||||
version: 1.5.6
|
version: 1.5.6
|
||||||
@@ -791,6 +797,11 @@ packages:
|
|||||||
engines: {node: '>=22.0.0'}
|
engines: {node: '>=22.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@capacitor/clipboard@8.0.1':
|
||||||
|
resolution: {integrity: sha512-iOlbTi8MojKyLnYE+M27priXid7vHd0PlDwyHohPzkuQ8Rkp6q7ykwZmPEUD+OnU/Ink7Qw/pUOfKgraKmA6Eg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@capacitor/core': '>=8.0.0'
|
||||||
|
|
||||||
'@capacitor/core@8.0.1':
|
'@capacitor/core@8.0.1':
|
||||||
resolution: {integrity: sha512-5UqSWxGMp/B8KhYu7rAijqNtYslhcLh+TrbfU48PfdMDsPfaU/VY48sMNzC22xL8BmoFoql/3SKyP+pavTOvOA==}
|
resolution: {integrity: sha512-5UqSWxGMp/B8KhYu7rAijqNtYslhcLh+TrbfU48PfdMDsPfaU/VY48sMNzC22xL8BmoFoql/3SKyP+pavTOvOA==}
|
||||||
|
|
||||||
@@ -5969,6 +5980,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@capacitor/clipboard@8.0.1(@capacitor/core@8.0.1)':
|
||||||
|
dependencies:
|
||||||
|
'@capacitor/core': 8.0.1
|
||||||
|
|
||||||
'@capacitor/core@8.0.1':
|
'@capacitor/core@8.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import {Clipboard} from "@capacitor/clipboard"
|
||||||
|
import {Capacitor} from "@capacitor/core"
|
||||||
|
import {Extension} from "@tiptap/core"
|
||||||
|
import {Plugin, PluginKey} from "@tiptap/pm/state"
|
||||||
|
|
||||||
|
const nativeClipboardAvailable = () =>
|
||||||
|
Capacitor.isNativePlatform() && Capacitor.isPluginAvailable("Clipboard")
|
||||||
|
|
||||||
|
const hasStandardPastePayload = (event: ClipboardEvent) => {
|
||||||
|
const clipboardData = event.clipboardData
|
||||||
|
|
||||||
|
if (!clipboardData) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.from(clipboardData.items).some(item => item.kind === "file")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clipboardData.types.includes("text/html")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return clipboardData.getData("text/plain") !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNativeClipboardImage = async () => {
|
||||||
|
try {
|
||||||
|
const {type, value} = await Clipboard.read()
|
||||||
|
|
||||||
|
if (!type.startsWith("image/") || value === "") {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageData = value.startsWith("data:") ? value : `data:${type};base64,${value}`
|
||||||
|
const blob = await fetch(imageData).then(res => res.blob())
|
||||||
|
|
||||||
|
if (!blob.type.startsWith("image/")) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = type.split("/")[1]?.split("+")[0] || "png"
|
||||||
|
|
||||||
|
return new File([blob], `clipboard-image.${extension}`, {type: blob.type || type})
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NativeClipboardPasteExtension = Extension.create({
|
||||||
|
name: "nativeClipboardPaste",
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
const editor = this.editor
|
||||||
|
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey("nativeClipboardPaste"),
|
||||||
|
props: {
|
||||||
|
handlePaste: (_view, event) => {
|
||||||
|
if (!nativeClipboardAvailable() || hasStandardPastePayload(event)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
void getNativeClipboardImage().then(file => {
|
||||||
|
if (!file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.commands.addFile(file, editor.state.selection.from + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {FileAttributes} from "@welshman/editor"
|
import type {FileAttributes} from "@welshman/editor"
|
||||||
import {Editor, MentionSuggestion, WelshmanExtension, editorProps} from "@welshman/editor"
|
import {Editor, MentionSuggestion, WelshmanExtension, editorProps} from "@welshman/editor"
|
||||||
|
import {NativeClipboardPasteExtension} from "@app/editor/clipboard"
|
||||||
import {escapeHtml} from "@lib/html"
|
import {escapeHtml} from "@lib/html"
|
||||||
import {makeMentionNodeView} from "@app/editor/MentionNodeView"
|
import {makeMentionNodeView} from "@app/editor/MentionNodeView"
|
||||||
import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
|
import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
|
||||||
@@ -137,6 +138,7 @@ export const makeEditor = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
NativeClipboardPasteExtension,
|
||||||
],
|
],
|
||||||
onUpdate({editor}) {
|
onUpdate({editor}) {
|
||||||
wordCount?.set(editor.storage.wordCount.words)
|
wordCount?.set(editor.storage.wordCount.words)
|
||||||
|
|||||||
Reference in New Issue
Block a user