flotilla/src/app/editor/index.ts

209 lines
6.4 KiB
TypeScript
Raw Normal View History

2025-02-05 03:00:48 +00:00
import {mount} from "svelte"
2025-01-02 18:04:28 +00:00
import type {Writable} from "svelte/store"
import {get, derived} from "svelte/store"
import {Router} from "@welshman/router"
import {dec, inc} from "@welshman/lib"
import {throttled} from "@welshman/store"
import type {PublishedProfile, RoomMeta} from "@welshman/util"
import {
createSearch,
profiles,
searchProfiles,
handlesByNip05,
2025-11-25 19:55:32 +00:00
getMaxWot,
getWotGraph,
} from "@welshman/app"
2025-06-06 23:38:11 +00:00
import type {FileAttributes} from "@welshman/editor"
import {
Editor,
MentionSuggestion,
TippySuggestion,
WelshmanExtension,
editorProps,
} from "@welshman/editor"
2026-02-16 16:16:03 +00:00
import {escapeHtml} from "@lib/html"
import {makeMentionNodeView} from "@app/editor/MentionNodeView"
import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
import {RoomReferenceExtension} from "@app/editor/RoomReferenceExtension"
import RoomSuggestion from "@app/editor/RoomSuggestion.svelte"
import {NativeClipboardPasteExtension} from "@app/editor/clipboard"
import {compressFileForUpload, uploadFile} from "@app/uploads"
import {deriveSpaceMembers} from "@app/members"
import {makeRoomId, splitRoomId, userSpaceUrls, roomsByUrl} from "@app/groups"
2026-06-08 21:14:40 +00:00
import {pushToast} from "@app/toast"
2025-01-02 18:04:28 +00:00
export const makeEditor = async ({
encryptFiles = false,
2025-01-17 23:01:05 +00:00
aggressive = false,
2025-01-02 18:04:28 +00:00
charCount,
content = "",
onChange,
2025-01-02 18:04:28 +00:00
placeholder = "",
2025-04-15 18:03:27 +00:00
url,
2025-01-02 18:04:28 +00:00
submit,
uploading,
wordCount,
}: {
encryptFiles?: boolean
2025-01-17 23:01:05 +00:00
aggressive?: boolean
2025-01-02 18:04:28 +00:00
charCount?: Writable<number>
content?: string | object
2026-04-07 13:06:29 +00:00
onChange?: (json: object) => void
2025-01-02 18:04:28 +00:00
placeholder?: string
2025-04-15 18:03:27 +00:00
url?: string
2025-01-02 18:04:28 +00:00
submit: () => void
uploading?: Writable<boolean>
wordCount?: Writable<number>
}) => {
const profileSearch = derived(
[
throttled(800, profiles),
throttled(800, handlesByNip05),
throttled(800, deriveSpaceMembers(url || "")),
],
([$profiles, $handlesByNip05, $spaceMembers]) => {
// Remove invalid nip05's from profiles
const options = $profiles.map(p => {
const isNip05Valid = !p.nip05 || $handlesByNip05.get(p.nip05)?.pubkey === p.event.pubkey
return isNip05Valid ? p : {...p, nip05: ""}
})
return createSearch(options, {
onSearch: searchProfiles,
getValue: (profile: PublishedProfile) => profile.event.pubkey,
sortFn: ({score = 1, item}) => {
2025-11-25 19:55:32 +00:00
const wotScore = getWotGraph().get(item.event.pubkey) || 0
const membershipScale = $spaceMembers?.includes(item.event.pubkey) ? 2 : 1
2025-11-25 19:55:32 +00:00
return dec(score) * inc(wotScore / getMaxWot()) * membershipScale
},
fuseOptions: {
keys: [
"nip05",
{name: "name", weight: 0.8},
{name: "display_name", weight: 0.5},
{name: "about", weight: 0.3},
],
threshold: 0.3,
shouldSort: false,
},
})
},
)
const roomReferenceSearch = derived(
[throttled(800, userSpaceUrls), throttled(800, roomsByUrl)],
([$userSpaceUrls, $roomsByUrl]) => {
const roomIdByMeta = new WeakMap<RoomMeta, string>()
const options: RoomMeta[] = []
for (const roomUrl of $userSpaceUrls) {
for (const room of $roomsByUrl.get(roomUrl) || []) {
roomIdByMeta.set(room, makeRoomId(roomUrl, room.h))
options.push(room)
}
}
return createSearch(options, {
getValue: item => roomIdByMeta.get(item) || item.h,
fuseOptions: {
keys: ["name", "h"],
threshold: 0.3,
shouldSort: false,
},
})
},
)
const ed = new Editor({
content: typeof content === "string" ? escapeHtml(content) : content,
2025-12-01 16:45:35 +00:00
editorProps,
2025-02-05 18:47:56 +00:00
element: document.createElement("div"),
2025-01-02 18:04:28 +00:00
extensions: [
RoomReferenceExtension,
2025-01-02 18:04:28 +00:00
WelshmanExtension.configure({
submit,
extensions: {
placeholder: {
config: {
placeholder,
},
},
2025-01-17 23:01:05 +00:00
breakOrSubmit: {
config: {
aggressive,
},
},
2025-06-03 17:41:38 +00:00
fileUpload: {
config: {
upload: async (attrs: FileAttributes) =>
uploadFile(await compressFileForUpload(attrs.file), {url, encrypt: encryptFiles}),
onDrop: () => uploading?.set(true),
onComplete: () => uploading?.set(false),
2025-06-06 23:38:11 +00:00
onUploadError(currentEditor, task) {
currentEditor.commands.removeFailedUploads()
2025-08-21 23:06:14 +00:00
pushToast({theme: "error", message: task.error})
2025-06-06 23:38:11 +00:00
uploading?.set(false)
2025-01-02 18:04:28 +00:00
},
2025-06-03 17:41:38 +00:00
},
},
2025-01-02 18:04:28 +00:00
nprofile: {
extend: {
2025-04-15 18:03:27 +00:00
addNodeView: () => makeMentionNodeView(url),
2025-01-02 18:04:28 +00:00
addProseMirrorPlugins() {
return [
MentionSuggestion({
editor: (this as any).editor,
2025-02-05 03:00:48 +00:00
search: (term: string) => get(profileSearch).searchValues(term),
2025-04-09 22:32:18 +00:00
getRelays: (pubkey: string) => Router.get().FromPubkeys([pubkey]).getUrls(),
updateSignal: profileSearch,
2025-02-05 03:37:59 +00:00
createSuggestion: (value: string) => {
2025-02-05 03:00:48 +00:00
const target = document.createElement("div")
2025-04-15 18:03:27 +00:00
mount(ProfileSuggestion, {target, props: {value, url}})
2025-02-05 03:00:48 +00:00
return target
},
}),
TippySuggestion({
char: "~",
name: "roomref",
editor: (this as any).editor,
search: (term: string) => get(roomReferenceSearch).searchValues(term),
updateSignal: roomReferenceSearch,
select: (id: string, props) => {
const [roomUrl, h] = splitRoomId(id)
if (!roomUrl || !h) {
return
}
return props.command({url: roomUrl, h})
},
createSuggestion: (value: string) => {
const target = document.createElement("div")
mount(RoomSuggestion, {target, props: {value}})
2025-02-05 03:00:48 +00:00
return target
},
2025-01-02 18:04:28 +00:00
}),
]
},
},
},
},
}),
NativeClipboardPasteExtension,
2025-01-02 18:04:28 +00:00
],
onUpdate({editor}) {
wordCount?.set(editor.storage.wordCount.words)
charCount?.set(editor.storage.wordCount.chars)
onChange?.(editor.getJSON())
2025-01-02 18:04:28 +00:00
},
})
return ed
}