diff --git a/src/app/components/ContentLinkBlock.svelte b/src/app/components/ContentLinkBlock.svelte index c818f1ad..fec50b30 100644 --- a/src/app/components/ContentLinkBlock.svelte +++ b/src/app/components/ContentLinkBlock.svelte @@ -5,30 +5,32 @@ import {preventDefault, stopPropagation} from "@lib/html" import Link from "@lib/components/Link.svelte" import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte" + import ContentLinkUrl from "@app/components/ContentLinkUrl.svelte" import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte" import {pushModal} from "@app/util/modal" import { dufflepud, - PLATFORM_URL, IMAGE_CONTENT_TYPES, + PLATFORM_URL, VIDEO_CONTENT_TYPES, THUMBNAIL_URL, + isRoomId, } from "@app/core/state" - import {makeSpacePath} from "@app/util/routes" const {value, event} = $props() let hideImage = $state(false) const url = value.url.toString() - const fileType = getTagValue("file-type", event.tags) || "" + const isRoomOrRelay = isRoomId(url) || isRelayUrl(url) const [href, external] = call(() => { - if (isRelayUrl(url)) return [makeSpacePath(url), false] if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false] return [url, true] }) + const fileType = getTagValue("file-type", event.tags) || "" + const getVideoPoster = (videoUrl: string): string | undefined => { if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) { return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}` @@ -54,46 +56,52 @@ const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true}) - -
- {#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)} - - {:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)} - - {:else} - {#await loadPreview()} -
- -
- {:then preview} -
- {#if preview.image && !hideImage} - - {/if} -
- {preview.title || displayUrl(url)} -

{ellipsize(preview.description, 140)}

-
-
- {:catch} -

- Unable to load a preview for {url} -

- {/await} - {/if} +{#if isRoomOrRelay} +
+
- +{:else} + +
+ {#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)} + + {:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)} + + {:else} + {#await loadPreview()} +
+ +
+ {:then preview} +
+ {#if preview.image && !hideImage} + + {/if} +
+ {preview.title || displayUrl(url)} +

{ellipsize(preview.description, 140)}

+
+
+ {:catch} +

+ Unable to load a preview for {url} +

+ {/await} + {/if} +
+ +{/if} diff --git a/src/app/components/ContentLinkInline.svelte b/src/app/components/ContentLinkInline.svelte index 949c5910..fec1055c 100644 --- a/src/app/components/ContentLinkInline.svelte +++ b/src/app/components/ContentLinkInline.svelte @@ -1,25 +1,18 @@ @@ -34,8 +27,5 @@ {displayUrl(url)} {:else} - - - {displayUrl(url)} - + {/if} diff --git a/src/app/components/ContentLinkUrl.svelte b/src/app/components/ContentLinkUrl.svelte new file mode 100644 index 00000000..38a25e12 --- /dev/null +++ b/src/app/components/ContentLinkUrl.svelte @@ -0,0 +1,59 @@ + + + + {#if roomReference} + ~{displayRelayUrl(roomReference.url)} / + {displayRoom(roomReference.url, roomReference.h)} + {:else if relayReference} + {displayRelayUrl(relayReference)} + {:else} + + {displayUrl(url)} + {/if} + diff --git a/src/app/components/RoomNameWithImage.svelte b/src/app/components/RoomNameWithImage.svelte index 4f8ff116..8ae407c9 100644 --- a/src/app/components/RoomNameWithImage.svelte +++ b/src/app/components/RoomNameWithImage.svelte @@ -12,7 +12,7 @@
-
+
diff --git a/src/app/core/state.ts b/src/app/core/state.ts index 59ea9e3c..767b16a9 100644 --- a/src/app/core/state.ts +++ b/src/app/core/state.ts @@ -593,6 +593,8 @@ export const getRoomType = (room: RoomMeta): RoomType => export const makeRoomId = (url: string, h: string) => `${url}'${h}` +export const isRoomId = (id: string) => id.includes("'") + export const splitRoomId = (id: string) => id.split("'") export const hasNip29 = (relay?: RelayProfile) => diff --git a/src/app/editor/RoomReferenceExtension.ts b/src/app/editor/RoomReferenceExtension.ts new file mode 100644 index 00000000..5a134764 --- /dev/null +++ b/src/app/editor/RoomReferenceExtension.ts @@ -0,0 +1,42 @@ +import {mergeAttributes, Node} from "@tiptap/core" +import {RoomReferenceNodeView} from "@app/editor/RoomReferenceNodeView" + +export const RoomReferenceExtension = Node.create({ + name: "roomref", + + atom: true, + + inline: true, + + group: "inline", + + selectable: true, + + priority: 1000, + + addAttributes() { + return { + url: {default: undefined}, + h: {default: undefined}, + } + }, + + parseHTML() { + return [{tag: `span[data-type="${this.name}"]`}] + }, + + renderHTML({HTMLAttributes}) { + return ["span", mergeAttributes(HTMLAttributes, {"data-type": this.name}), "~"] + }, + + renderText({node}) { + const url = typeof node.attrs.url === "string" ? node.attrs.url : "" + const h = typeof node.attrs.h === "string" ? node.attrs.h : "" + + return `${url}'${h}` + }, + + addNodeView() { + return RoomReferenceNodeView + }, +}) diff --git a/src/app/editor/RoomReferenceNodeView.ts b/src/app/editor/RoomReferenceNodeView.ts new file mode 100644 index 00000000..146e51f5 --- /dev/null +++ b/src/app/editor/RoomReferenceNodeView.ts @@ -0,0 +1,29 @@ +import type {NodeViewRendererProps} from "@tiptap/core" +import {displayRelayUrl} from "@welshman/util" +import {deriveRoom} from "@app/core/state" + +export const RoomReferenceNodeView = ({node}: NodeViewRendererProps) => { + const dom = document.createElement("span") + const url = typeof node.attrs.url === "string" ? node.attrs.url : "" + const h = typeof node.attrs.h === "string" ? node.attrs.h : "" + const room = deriveRoom(url, h) + + dom.classList.add("tiptap-object") + + const unsubRoom = room.subscribe($room => { + dom.textContent = `~${displayRelayUrl(url)} / ${$room.name || h}` + }) + + return { + dom, + destroy: () => { + unsubRoom() + }, + selectNode() { + dom.classList.add("tiptap-active") + }, + deselectNode() { + dom.classList.remove("tiptap-active") + }, + } +} diff --git a/src/app/editor/RoomSuggestion.svelte b/src/app/editor/RoomSuggestion.svelte new file mode 100644 index 00000000..89f4853f --- /dev/null +++ b/src/app/editor/RoomSuggestion.svelte @@ -0,0 +1,17 @@ + + +
+ + {displayRelayUrl(url)} +
diff --git a/src/app/editor/index.ts b/src/app/editor/index.ts index b3a8c858..c4e5df3f 100644 --- a/src/app/editor/index.ts +++ b/src/app/editor/index.ts @@ -4,7 +4,7 @@ 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} from "@welshman/util" +import type {PublishedProfile, RoomMeta} from "@welshman/util" import { createSearch, profiles, @@ -14,13 +14,27 @@ import { getWotGraph, } from "@welshman/app" import type {FileAttributes} from "@welshman/editor" -import {Editor, MentionSuggestion, WelshmanExtension, editorProps} from "@welshman/editor" -import {NativeClipboardPasteExtension} from "@app/editor/clipboard" +import { + Editor, + MentionSuggestion, + TippySuggestion, + WelshmanExtension, + editorProps, +} from "@welshman/editor" 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 {uploadFile} from "@app/core/commands" -import {deriveSpaceMembers} from "@app/core/state" +import { + deriveSpaceMembers, + makeRoomId, + splitRoomId, + userSpaceUrls, + roomsByUrl, +} from "@app/core/state" import {pushToast} from "@app/util/toast" export const makeEditor = async ({ @@ -83,11 +97,36 @@ export const makeEditor = async ({ }, ) + const roomReferenceSearch = derived( + [throttled(800, userSpaceUrls), throttled(800, roomsByUrl)], + ([$userSpaceUrls, $roomsByUrl]) => { + const roomIdByMeta = new WeakMap() + 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, editorProps, element: document.createElement("div"), extensions: [ + RoomReferenceExtension, WelshmanExtension.configure({ submit, extensions: { @@ -129,6 +168,29 @@ export const makeEditor = async ({ mount(ProfileSuggestion, {target, props: {value, url}}) + 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}}) + return target }, }), diff --git a/src/app/util/routes.ts b/src/app/util/routes.ts index 998834ec..1a5371ae 100644 --- a/src/app/util/routes.ts +++ b/src/app/util/routes.ts @@ -63,11 +63,11 @@ export const goToSpace = async (url: string) => { const prevPath = lastPageBySpaceUrl.get(encodeRelay(url)) if (prevPath && prevPath !== makeSpacePath(url)) { - goto(prevPath) + goto(prevPath, {replaceState: true}) } else if (window.matchMedia(`(min-width: ${theme.screens.md})`).matches) { - goto(makeSpacePath(url, "recent")) + goto(makeSpacePath(url, "recent"), {replaceState: true}) } else { - goto(makeSpacePath(url)) + goto(makeSpacePath(url), {replaceState: true}) } }