Files
flotilla/src/app/editor/index.ts
T
2025-06-09 13:48:46 -07:00

175 lines
5.4 KiB
TypeScript

import {mount} from "svelte"
import type {Writable} from "svelte/store"
import {get} from "svelte/store"
import {sha256} from "@welshman/lib"
import {
getTagValues,
encryptFile,
uploadBlob,
makeBlossomAuthEvent,
getListTags,
} from "@welshman/util"
import {Router} from "@welshman/router"
import {Nip01Signer} from "@welshman/signer"
import {signer, profileSearch, userBlossomServers} from "@welshman/app"
import type {FileAttributes} from "@welshman/editor"
import {Editor, MentionSuggestion, WelshmanExtension} from "@welshman/editor"
import {makeMentionNodeView} from "./MentionNodeView"
import ProfileSuggestion from "./ProfileSuggestion.svelte"
import {pushToast} from "@app/toast"
export const getBlossomServer = () => {
const userUrls = getTagValues("server", getListTags(userBlossomServers.get()))
for (const url of userUrls) {
return url.replace(/^ws/, "http")
}
return "https://cdn.satellite.earth"
}
export const makeEditor = async ({
aggressive = false,
autofocus = false,
charCount,
content = "",
placeholder = "",
url,
submit,
uploading,
wordCount,
}: {
aggressive?: boolean
autofocus?: boolean
charCount?: Writable<number>
content?: string
placeholder?: string
url?: string
submit: () => void
uploading?: Writable<boolean>
wordCount?: Writable<number>
}) => {
return new Editor({
content,
autofocus,
element: document.createElement("div"),
extensions: [
WelshmanExtension.configure({
submit,
extensions: {
placeholder: {
config: {
placeholder,
},
},
breakOrSubmit: {
config: {
aggressive,
},
},
fileUpload: {
config: {
upload: async (attrs: FileAttributes) => {
let file: Blob = attrs.file
if (!file.type.match("image/(webp|gif)")) {
const {default: Compressor} = await import("compressorjs")
file = await new Promise((resolve, _reject) => {
new Compressor(file, {
maxWidth: 1024,
maxHeight: 1024,
convertSize: 2 * 1024 * 1024,
success: resolve,
error: e => {
// Non-images break compressor
if (e.toString().includes("File or Blob")) {
return resolve(file)
}
_reject(e)
},
})
})
}
const {ciphertext, key, nonce, algorithm} = await encryptFile(file)
const tags = [
["decryption-key", key],
["decryption-nonce", nonce],
["encryption-algorithm", algorithm],
]
file = new File([new Blob([ciphertext])], attrs.file.name, {type: attrs.file.type})
const server = getBlossomServer()
const hashes = [await sha256(await file.arrayBuffer())]
const $signer = signer.get() || Nip01Signer.ephemeral()
const authTemplate = makeBlossomAuthEvent({action: "upload", server, hashes})
const authEvent = await $signer.sign(authTemplate)
try {
const res = await uploadBlob(server, file, {authEvent})
let {uploaded, url, ...task} = await res.json()
if (!uploaded) {
return {error: "Server refused to process the file"}
}
// Always append file extension if missing
if (new URL(url).pathname.split(".").length === 1) {
url += "." + attrs.file.type.split("/")[1]
}
const result = {...task, tags, url}
return {result}
} catch (e: any) {
console.error(e)
return {error: e.toString()}
}
},
onDrop() {
uploading?.set(true)
},
onComplete() {
uploading?.set(false)
},
onUploadError(currentEditor, task) {
currentEditor.commands.removeFailedUploads()
pushToast({theme: "error", message: "Failed to upload file"})
uploading?.set(false)
},
},
},
nprofile: {
extend: {
addNodeView: () => makeMentionNodeView(url),
addProseMirrorPlugins() {
return [
MentionSuggestion({
editor: (this as any).editor,
search: (term: string) => get(profileSearch).searchValues(term),
getRelays: (pubkey: string) => Router.get().FromPubkeys([pubkey]).getUrls(),
createSuggestion: (value: string) => {
const target = document.createElement("div")
mount(ProfileSuggestion, {target, props: {value, url}})
return target
},
}),
]
},
},
},
},
}),
],
onUpdate({editor}) {
wordCount?.set(editor.storage.wordCount.words)
charCount?.set(editor.storage.wordCount.chars)
},
})
}