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})
}
}