forked from coracle/flotilla
Compare commits
6 Commits
0c40be9cfc
...
540e9abe3d
| Author | SHA1 | Date | |
|---|---|---|---|
| 540e9abe3d | |||
| ddb8391b02 | |||
| 12fd552e1b | |||
| 65ca8a7fd8 | |||
| 7f1e98dcb2 | |||
| 4c19ee823b |
@@ -19,5 +19,6 @@ VITE_DEFAULT_SEARCH_RELAYS=relay.ditto.pub,antiprimal.net,relay.vertexlab.io
|
||||
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,relay.keychat.io,relay.ditto.pub
|
||||
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
|
||||
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
||||
VITE_THUMBNAIL_URL=
|
||||
VITE_GLITCHTIP_API_KEY=
|
||||
GLITCHTIP_AUTH_TOKEN=
|
||||
|
||||
@@ -28,6 +28,7 @@ node_modules/
|
||||
.pnpm-store/
|
||||
build/
|
||||
.svelte-kit/
|
||||
.next/
|
||||
|
||||
# Rust/Tauri
|
||||
*target/
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
@import 'tailwindcss';
|
||||
@import "tailwindcss";
|
||||
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
}
|
||||
|
||||
@utility content-padding-y {
|
||||
@apply pt-4 sm:pt-8 md:pt-12 pb-4 sm:pb-8 md:pb-12;
|
||||
@apply pt-4 pb-4 sm:pt-8 sm:pb-8 md:pt-12 md:pb-12;
|
||||
}
|
||||
|
||||
@utility content-sizing {
|
||||
@@ -139,7 +139,7 @@
|
||||
}
|
||||
|
||||
@utility content {
|
||||
@apply m-auto w-full max-w-3xl px-4 sm:px-8 md:px-12 pt-4 sm:pt-8 md:pt-12 pb-4 sm:pb-8 md:pb-12;
|
||||
@apply m-auto w-full max-w-3xl px-4 pt-4 pb-4 sm:px-8 sm:pt-8 sm:pb-8 md:px-12 md:pt-12 md:pb-12;
|
||||
}
|
||||
|
||||
@utility heading {
|
||||
|
||||
@@ -1,27 +1,85 @@
|
||||
<script lang="ts">
|
||||
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
|
||||
import {isRelayUrl, getTagValue} from "@welshman/util"
|
||||
import {isRelayUrl, getTagValue, normalizeRelayUrl, displayRelayUrl} from "@welshman/util"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
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 ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {dufflepud, PLATFORM_URL, IMAGE_CONTENT_TYPES, VIDEO_CONTENT_TYPES} from "@app/core/state"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {
|
||||
dufflepud,
|
||||
PLATFORM_URL,
|
||||
IMAGE_CONTENT_TYPES,
|
||||
VIDEO_CONTENT_TYPES,
|
||||
THUMBNAIL_URL,
|
||||
displayRoom,
|
||||
isRoomId,
|
||||
splitRoomId,
|
||||
} from "@app/core/state"
|
||||
import {makeRoomPath, makeSpacePath} from "@app/util/routes"
|
||||
|
||||
const {value, event} = $props()
|
||||
|
||||
let hideImage = $state(false)
|
||||
|
||||
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 [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]
|
||||
|
||||
return [url, true]
|
||||
})
|
||||
|
||||
const getVideoPoster = (videoUrl: string): string | undefined => {
|
||||
if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) {
|
||||
return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}`
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const loadPreview = async () => {
|
||||
const json = await postJson(dufflepud("link/preview"), {url})
|
||||
|
||||
@@ -42,13 +100,23 @@
|
||||
<Link {external} {href} class="my-2 block">
|
||||
<div class="overflow-hidden rounded-box">
|
||||
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
|
||||
<video controls src={url} class="max-h-96 rounded-box object-contain object-center">
|
||||
<video
|
||||
controls
|
||||
src={url}
|
||||
poster={getVideoPoster(url)}
|
||||
preload="metadata"
|
||||
class="max-h-96 rounded-box object-contain object-center">
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
|
||||
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
|
||||
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
|
||||
</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}
|
||||
{#await loadPreview()}
|
||||
<div class="center my-12 w-full">
|
||||
|
||||
@@ -1,21 +1,65 @@
|
||||
<script lang="ts">
|
||||
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 LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {PLATFORM_URL, IMAGE_CONTENT_TYPES} from "@app/core/state"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {
|
||||
PLATFORM_URL,
|
||||
IMAGE_CONTENT_TYPES,
|
||||
displayRoom,
|
||||
isRoomId,
|
||||
splitRoomId,
|
||||
} from "@app/core/state"
|
||||
import {makeRoomPath, makeSpacePath} from "@app/util/routes"
|
||||
|
||||
const {value, event} = $props()
|
||||
|
||||
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 [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]
|
||||
|
||||
return [url, true]
|
||||
@@ -36,6 +80,6 @@
|
||||
{:else}
|
||||
<Link {external} {href} class="link-content whitespace-nowrap">
|
||||
<Icon icon={LinkRound} size={3} class="inline-block" />
|
||||
{displayUrl(url)}
|
||||
{label}
|
||||
</Link>
|
||||
{/if}
|
||||
|
||||
@@ -62,8 +62,7 @@
|
||||
{@render children?.()}
|
||||
|
||||
<!-- a little extra something for ios -->
|
||||
<div
|
||||
class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-base-100 md:hidden">
|
||||
<div class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-base-100 md:hidden">
|
||||
</div>
|
||||
<div
|
||||
class="hide-on-keyboard border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
||||
|
||||
@@ -210,6 +210,8 @@ export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
|
||||
|
||||
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
|
||||
|
||||
export const THUMBNAIL_URL = import.meta.env.VITE_THUMBNAIL_URL
|
||||
|
||||
export const NIP46_PERMS =
|
||||
"nip44_encrypt,nip44_decrypt," +
|
||||
[
|
||||
@@ -594,6 +596,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) =>
|
||||
|
||||
+74
-64
@@ -1,8 +1,8 @@
|
||||
import {page} from "$app/stores"
|
||||
import type {Unsubscriber} from "svelte/store"
|
||||
import {derived, get} from "svelte/store"
|
||||
import {last, call, ifLet, assoc, chunk, sleep, identity, WEEK, ago} from "@welshman/lib"
|
||||
import {last, call, ifLet, assoc, chunk, WEEK, ago} from "@welshman/lib"
|
||||
import {PollResponse} from "nostr-tools/kinds"
|
||||
import {merged} from "@welshman/store"
|
||||
import {
|
||||
getListTags,
|
||||
getRelayTagValues,
|
||||
@@ -21,12 +21,11 @@ import {
|
||||
unionFilters,
|
||||
getTagValue,
|
||||
} from "@welshman/util"
|
||||
import type {Filter, TrustedEvent} from "@welshman/util"
|
||||
import type {Filter, List, PublishedList, TrustedEvent} from "@welshman/util"
|
||||
import {request, requestOne, Difference, DifferenceEvent} from "@welshman/net"
|
||||
import {
|
||||
pubkey,
|
||||
loadRelay,
|
||||
userFollowList,
|
||||
userRelayList,
|
||||
userMessagingRelayList,
|
||||
loadRelayList,
|
||||
@@ -49,7 +48,6 @@ import {
|
||||
loadGroupList,
|
||||
userSpaceUrls,
|
||||
userGroupList,
|
||||
bootstrapPubkeys,
|
||||
decodeRelay,
|
||||
getSpaceUrlsFromGroupList,
|
||||
getSpaceRoomsFromGroupList,
|
||||
@@ -74,6 +72,8 @@ const pullOneWithFallback = async (
|
||||
signal: AbortSignal,
|
||||
onEvent?: (event: TrustedEvent) => void,
|
||||
) => {
|
||||
if (signal.aborted) return
|
||||
|
||||
const cachedEvents = repository.query([filter]).filter(isSignedEvent)
|
||||
const since = last(cachedEvents.slice(10))?.created_at || 0
|
||||
|
||||
@@ -86,6 +86,12 @@ const pullOneWithFallback = async (
|
||||
const shouldFallback =
|
||||
!hasNegentropy(url) ||
|
||||
(await new Promise(resolve => {
|
||||
if (signal.aborted) {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
|
||||
// If teardown wins while the diff is opening, skip the fallback path and let cleanup stay in control.
|
||||
const diff = new Difference({relay: url, filter, events: cachedEvents, signal})
|
||||
|
||||
diff.on(DifferenceEvent.Error, () => {
|
||||
@@ -111,9 +117,7 @@ export const pullWithFallback = async ({url, signal, filters, onEvent}: SyncOpts
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
for (const filter of filters) {
|
||||
pullOneWithFallback(url, filter, signal, onEvent)
|
||||
}
|
||||
await Promise.all(filters.map(filter => pullOneWithFallback(url, filter, signal, onEvent)))
|
||||
}
|
||||
|
||||
const listen = ({url, signal, filters, onEvent}: SyncOpts) => {
|
||||
@@ -123,6 +127,8 @@ const listen = ({url, signal, filters, onEvent}: SyncOpts) => {
|
||||
}
|
||||
|
||||
const pullAndListen = (options: SyncOpts) => {
|
||||
if (options.signal.aborted) return
|
||||
|
||||
pullWithFallback(options)
|
||||
listen(options)
|
||||
}
|
||||
@@ -197,7 +203,7 @@ const syncUserRoomMembership = (url: string, h: string) => {
|
||||
const syncUserData = () => {
|
||||
const unsubscribersByKey = new Map<string, Unsubscriber>()
|
||||
|
||||
const unsubscribeGroupList = userGroupList.subscribe($userGroupList => {
|
||||
const syncGroupList = ($userGroupList: List | undefined) => {
|
||||
if ($userGroupList) {
|
||||
const keys = new Set<string>()
|
||||
|
||||
@@ -226,43 +232,35 @@ const syncUserData = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const syncRelayList = ($userRelayList: PublishedList | undefined) => {
|
||||
const pubkey = $userRelayList?.event?.pubkey
|
||||
|
||||
if (!pubkey) return
|
||||
|
||||
loadBlossomServerList(pubkey)
|
||||
loadBlockedRelayList(pubkey)
|
||||
loadFollowList(pubkey)
|
||||
loadGroupList(pubkey)
|
||||
loadMuteList(pubkey)
|
||||
loadProfile(pubkey)
|
||||
loadSettings(pubkey)
|
||||
loadFeedsForPubkey(pubkey)
|
||||
}
|
||||
|
||||
const unsubscribeGroupList = merged([userGroupList]).subscribe(([$userGroupList]) => {
|
||||
syncGroupList($userGroupList)
|
||||
})
|
||||
|
||||
const unsubscribeRelayList = userRelayList.subscribe($userRelayList => {
|
||||
if ($userRelayList) {
|
||||
loadBlossomServerList($userRelayList.event.pubkey)
|
||||
loadBlockedRelayList($userRelayList.event.pubkey)
|
||||
loadFollowList($userRelayList.event.pubkey)
|
||||
loadGroupList($userRelayList.event.pubkey)
|
||||
loadMuteList($userRelayList.event.pubkey)
|
||||
loadProfile($userRelayList.event.pubkey)
|
||||
loadSettings($userRelayList.event.pubkey)
|
||||
loadFeedsForPubkey($userRelayList.event.pubkey)
|
||||
}
|
||||
})
|
||||
|
||||
const unsubscribeFollows = userFollowList.subscribe(async $userFollowList => {
|
||||
for (const pubkeys of chunk(10, get(bootstrapPubkeys))) {
|
||||
// This isn't urgent, avoid clogging other stuff up
|
||||
await sleep(1000)
|
||||
|
||||
await Promise.all(
|
||||
pubkeys.flatMap(pk => [
|
||||
loadRelayList(pk),
|
||||
loadGroupList(pk),
|
||||
loadProfile(pk),
|
||||
loadFollowList(pk),
|
||||
loadMuteList(pk),
|
||||
]),
|
||||
)
|
||||
}
|
||||
const unsubscribeRelayList = merged([userRelayList]).subscribe(([$userRelayList]) => {
|
||||
syncRelayList($userRelayList)
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribersByKey.forEach(call)
|
||||
unsubscribeGroupList()
|
||||
unsubscribeRelayList()
|
||||
unsubscribeFollows()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +319,7 @@ const syncSpace = (url: string, rooms: string[]) => {
|
||||
}
|
||||
|
||||
const syncSpaces = () => {
|
||||
const store = derived([userGroupList, page], identity)
|
||||
const store = merged([userGroupList, page])
|
||||
const unsubscribersByUrl = new Map<string, Unsubscriber>()
|
||||
const roomsByUrl = new Map<string, string>()
|
||||
|
||||
@@ -383,6 +381,7 @@ const syncDMs = () => {
|
||||
const unsubscribersByUrl = new Map<string, Unsubscriber>()
|
||||
|
||||
let currentPubkey: string | undefined
|
||||
let currentShouldUnwrap = false
|
||||
|
||||
const unsubscribeAll = () => {
|
||||
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
|
||||
@@ -391,6 +390,34 @@ const syncDMs = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const syncPubkey = ($pubkey: string | undefined, $shouldUnwrap: boolean) => {
|
||||
if ($pubkey !== currentPubkey) {
|
||||
unsubscribeAll()
|
||||
}
|
||||
|
||||
if ($pubkey && $shouldUnwrap) {
|
||||
loadRelayList($pubkey)
|
||||
.then(() => loadMessagingRelayList($pubkey))
|
||||
.then($l => {
|
||||
if ($l && currentPubkey === $pubkey && currentShouldUnwrap === $shouldUnwrap) {
|
||||
subscribeAll($pubkey, getRelayTagValues(getListTags($l)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
currentPubkey = $pubkey
|
||||
currentShouldUnwrap = $shouldUnwrap
|
||||
}
|
||||
|
||||
const syncList = ($userMessagingRelayList: List | undefined) => {
|
||||
const $pubkey = pubkey.get()
|
||||
const $shouldUnwrap = shouldUnwrap.get()
|
||||
|
||||
if ($pubkey && $shouldUnwrap) {
|
||||
subscribeAll($pubkey, getRelayTagValues(getListTags($userMessagingRelayList)))
|
||||
}
|
||||
}
|
||||
|
||||
const subscribeAll = (pubkey: string, urls: string[]) => {
|
||||
// Start syncing newly added relays
|
||||
for (const url of urls) {
|
||||
@@ -408,33 +435,16 @@ const syncDMs = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// When pubkey changes, re-sync
|
||||
const unsubscribePubkey = derived([pubkey, shouldUnwrap], identity).subscribe(
|
||||
([$pubkey, $shouldUnwrap]) => {
|
||||
if ($pubkey !== currentPubkey) {
|
||||
unsubscribeAll()
|
||||
}
|
||||
|
||||
// If we have a pubkey, refresh our user's relay list then sync our subscriptions
|
||||
if ($pubkey && $shouldUnwrap) {
|
||||
loadRelayList($pubkey)
|
||||
.then(() => loadMessagingRelayList($pubkey))
|
||||
.then($l => subscribeAll($pubkey, getRelayTagValues(getListTags($l))))
|
||||
}
|
||||
|
||||
currentPubkey = $pubkey
|
||||
},
|
||||
)
|
||||
const unsubscribePubkey = merged([pubkey, shouldUnwrap]).subscribe(([$pubkey, $shouldUnwrap]) => {
|
||||
syncPubkey($pubkey, $shouldUnwrap)
|
||||
})
|
||||
|
||||
// When user messaging relays change, update synchronization
|
||||
const unsubscribeList = userMessagingRelayList.subscribe($userMessagingRelayList => {
|
||||
const $pubkey = pubkey.get()
|
||||
const $shouldUnwrap = shouldUnwrap.get()
|
||||
|
||||
if ($pubkey && $shouldUnwrap) {
|
||||
subscribeAll($pubkey, getRelayTagValues(getListTags($userMessagingRelayList)))
|
||||
}
|
||||
})
|
||||
const unsubscribeList = merged([userMessagingRelayList]).subscribe(
|
||||
([$userMessagingRelayList]) => {
|
||||
syncList($userMessagingRelayList)
|
||||
},
|
||||
)
|
||||
|
||||
return () => {
|
||||
unsubscribeAll()
|
||||
|
||||
@@ -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>
|
||||
+65
-3
@@ -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,12 +14,26 @@ import {
|
||||
getWotGraph,
|
||||
} from "@welshman/app"
|
||||
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 {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 {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 ({
|
||||
@@ -82,11 +96,36 @@ export const makeEditor = async ({
|
||||
},
|
||||
)
|
||||
|
||||
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,
|
||||
editorProps,
|
||||
element: document.createElement("div"),
|
||||
extensions: [
|
||||
RoomReferenceExtension,
|
||||
WelshmanExtension.configure({
|
||||
submit,
|
||||
extensions: {
|
||||
@@ -128,6 +167,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
|
||||
},
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user