forked from coracle/flotilla
Compare commits
3 Commits
dev
...
02bd19490c
| Author | SHA1 | Date | |
|---|---|---|---|
| 02bd19490c | |||
| a4544c1e32 | |||
| bf2dfdc2d0 |
@@ -1,22 +1,70 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
|
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
|
||||||
import {isRelayUrl, getTagValue} from "@welshman/util"
|
import {isRelayUrl, getTagValue, normalizeRelayUrl, displayRelayUrl} from "@welshman/util"
|
||||||
import {preventDefault, stopPropagation} from "@lib/html"
|
import {preventDefault, stopPropagation} from "@lib/html"
|
||||||
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||||
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {dufflepud, PLATFORM_URL, IMAGE_CONTENT_TYPES, VIDEO_CONTENT_TYPES} from "@app/core/state"
|
import {
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
dufflepud,
|
||||||
|
PLATFORM_URL,
|
||||||
|
IMAGE_CONTENT_TYPES,
|
||||||
|
VIDEO_CONTENT_TYPES,
|
||||||
|
displayRoom,
|
||||||
|
isRoomId,
|
||||||
|
splitRoomId,
|
||||||
|
} from "@app/core/state"
|
||||||
|
import {makeRoomPath, makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
const {value, event} = $props()
|
const {value, event} = $props()
|
||||||
|
|
||||||
let hideImage = $state(false)
|
let hideImage = $state(false)
|
||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
const roomReference = call(() => {
|
||||||
|
if (!isRoomId(url)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const [roomUrl, h] = splitRoomId(url)
|
||||||
|
|
||||||
|
if (!roomUrl || !h || !isRelayUrl(roomUrl)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {url: normalizeRelayUrl(roomUrl), h}
|
||||||
|
})
|
||||||
|
|
||||||
|
const relayReference = call(() => {
|
||||||
|
if (roomReference || !isRelayUrl(url)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeRelayUrl(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
const label = call(() => {
|
||||||
|
if (roomReference) {
|
||||||
|
const spaceName = displayRelayUrl(roomReference.url)
|
||||||
|
const roomName = displayRoom(roomReference.url, roomReference.h)
|
||||||
|
|
||||||
|
return `~${spaceName} / ${roomName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relayReference) {
|
||||||
|
return `~${displayRelayUrl(relayReference)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayUrl(url)
|
||||||
|
})
|
||||||
|
|
||||||
const fileType = getTagValue("file-type", event.tags) || ""
|
const fileType = getTagValue("file-type", event.tags) || ""
|
||||||
const [href, external] = call(() => {
|
const [href, external] = call(() => {
|
||||||
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
if (roomReference) return [makeRoomPath(roomReference.url, roomReference.h), false]
|
||||||
|
if (relayReference) return [makeSpacePath(relayReference), false]
|
||||||
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
||||||
|
|
||||||
return [url, true]
|
return [url, true]
|
||||||
@@ -49,6 +97,11 @@
|
|||||||
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
|
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
|
||||||
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
|
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
|
||||||
</button>
|
</button>
|
||||||
|
{:else if roomReference || relayReference}
|
||||||
|
<div class="bg-alt p-4 leading-normal">
|
||||||
|
<Icon icon={LinkRound} size={3} class="inline-block" />
|
||||||
|
<span class="ml-2">{label}</span>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#await loadPreview()}
|
{#await loadPreview()}
|
||||||
<div class="center my-12 w-full">
|
<div class="center my-12 w-full">
|
||||||
|
|||||||
@@ -1,21 +1,65 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {call, displayUrl} from "@welshman/lib"
|
import {call, displayUrl} from "@welshman/lib"
|
||||||
import {isRelayUrl, getTagValue} from "@welshman/util"
|
import {isRelayUrl, getTagValue, normalizeRelayUrl, displayRelayUrl} from "@welshman/util"
|
||||||
import {preventDefault, stopPropagation} from "@lib/html"
|
import {preventDefault, stopPropagation} from "@lib/html"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {PLATFORM_URL, IMAGE_CONTENT_TYPES} from "@app/core/state"
|
import {
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
PLATFORM_URL,
|
||||||
|
IMAGE_CONTENT_TYPES,
|
||||||
|
displayRoom,
|
||||||
|
isRoomId,
|
||||||
|
splitRoomId,
|
||||||
|
} from "@app/core/state"
|
||||||
|
import {makeRoomPath, makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
const {value, event} = $props()
|
const {value, event} = $props()
|
||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
const roomReference = call(() => {
|
||||||
|
if (!isRoomId(url)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const [roomUrl, h] = splitRoomId(url)
|
||||||
|
|
||||||
|
if (!roomUrl || !h || !isRelayUrl(roomUrl)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {url: normalizeRelayUrl(roomUrl), h}
|
||||||
|
})
|
||||||
|
|
||||||
|
const relayReference = call(() => {
|
||||||
|
if (roomReference || !isRelayUrl(url)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeRelayUrl(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
const label = call(() => {
|
||||||
|
if (roomReference) {
|
||||||
|
const spaceName = displayRelayUrl(roomReference.url)
|
||||||
|
const roomName = displayRoom(roomReference.url, roomReference.h)
|
||||||
|
|
||||||
|
return `~${spaceName} / ${roomName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relayReference) {
|
||||||
|
return `~${displayRelayUrl(relayReference)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayUrl(url)
|
||||||
|
})
|
||||||
|
|
||||||
const fileType = getTagValue("file-type", event.tags) || ""
|
const fileType = getTagValue("file-type", event.tags) || ""
|
||||||
const [href, external] = call(() => {
|
const [href, external] = call(() => {
|
||||||
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
if (roomReference) return [makeRoomPath(roomReference.url, roomReference.h), false]
|
||||||
|
if (relayReference) return [makeSpacePath(relayReference), false]
|
||||||
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
||||||
|
|
||||||
return [url, true]
|
return [url, true]
|
||||||
@@ -36,6 +80,6 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<Link {external} {href} class="link-content whitespace-nowrap">
|
<Link {external} {href} class="link-content whitespace-nowrap">
|
||||||
<Icon icon={LinkRound} size={3} class="inline-block" />
|
<Icon icon={LinkRound} size={3} class="inline-block" />
|
||||||
{displayUrl(url)}
|
{label}
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -594,6 +594,8 @@ export const getRoomType = (room: RoomMeta): RoomType =>
|
|||||||
|
|
||||||
export const makeRoomId = (url: string, h: string) => `${url}'${h}`
|
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 splitRoomId = (id: string) => id.split("'")
|
||||||
|
|
||||||
export const hasNip29 = (relay?: RelayProfile) =>
|
export const hasNip29 = (relay?: RelayProfile) =>
|
||||||
|
|||||||
@@ -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
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
import {deriveRoom, splitRoomId} from "@app/core/state"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {value}: Props = $props()
|
||||||
|
const [url = "", h = ""] = splitRoomId(value)
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
|
||||||
|
const label = $derived(`~${displayRelayUrl(url)} / ${$room.name || h}`)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="max-w-full overflow-hidden text-ellipsis text-base font-semibold">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
+62
-2
@@ -14,12 +14,26 @@ import {
|
|||||||
getWotGraph,
|
getWotGraph,
|
||||||
} 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,
|
||||||
|
TippySuggestion,
|
||||||
|
WelshmanExtension,
|
||||||
|
editorProps,
|
||||||
|
} from "@welshman/editor"
|
||||||
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"
|
||||||
|
import {RoomReferenceExtension} from "@app/editor/RoomReferenceExtension"
|
||||||
|
import RoomSuggestion from "@app/editor/RoomSuggestion.svelte"
|
||||||
import {uploadFile} from "@app/core/commands"
|
import {uploadFile} from "@app/core/commands"
|
||||||
import {deriveSpaceMembers} from "@app/core/state"
|
import {
|
||||||
|
deriveSpaceMembers,
|
||||||
|
splitRoomId,
|
||||||
|
userSpaceUrls,
|
||||||
|
roomsByUrl,
|
||||||
|
type Room,
|
||||||
|
} from "@app/core/state"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
export const makeEditor = async ({
|
export const makeEditor = async ({
|
||||||
@@ -82,12 +96,35 @@ export const makeEditor = async ({
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const roomReferenceSearch = derived(
|
||||||
|
[throttled(800, userSpaceUrls), throttled(800, roomsByUrl)],
|
||||||
|
([$userSpaceUrls, $roomsByUrl]) => {
|
||||||
|
const options: Room[] = []
|
||||||
|
|
||||||
|
for (const roomUrl of $userSpaceUrls) {
|
||||||
|
for (const room of $roomsByUrl.get(roomUrl) || []) {
|
||||||
|
options.push(room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createSearch(options, {
|
||||||
|
getValue: item => item.id,
|
||||||
|
fuseOptions: {
|
||||||
|
keys: ["name", "h", "url"],
|
||||||
|
threshold: 0.3,
|
||||||
|
shouldSort: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return new Editor({
|
return new Editor({
|
||||||
content: escapeHtml(content),
|
content: escapeHtml(content),
|
||||||
autofocus,
|
autofocus,
|
||||||
editorProps,
|
editorProps,
|
||||||
element: document.createElement("div"),
|
element: document.createElement("div"),
|
||||||
extensions: [
|
extensions: [
|
||||||
|
RoomReferenceExtension,
|
||||||
WelshmanExtension.configure({
|
WelshmanExtension.configure({
|
||||||
submit,
|
submit,
|
||||||
extensions: {
|
extensions: {
|
||||||
@@ -129,6 +166,29 @@ export const makeEditor = async ({
|
|||||||
|
|
||||||
mount(ProfileSuggestion, {target, props: {value, url}})
|
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
|
return target
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user