feat: implement room and space mentions (#130) #154

Closed
Khushvendra wants to merge 5 commits from Khushvendra/flotilla:feat/room-mentions-130 into dev
Contributor

Summary

This PR introduces first-class support for Room and Space mentions (Issue #130), mirroring the existing pubkey mentions behavior. It adds full rich-text editor integration via Tiptap, content parsing for display, and supporting unit tests.

Users can now type ~ anywhere in the compose menu to trigger a room suggestion search overlay, which securely references NIP-29 rooms using the relay_url'room_id format and renders appropriately across the application.

Resolves #130.

Changes Included

  • Tiptap Editor Integration:
    • Added RoomReferenceExtension and RoomReferenceNodeView to safely encode room mentions into the relay_url'room_id reference format.
    • Implemented roomReferenceSearch in editor/index.ts to power the suggestion menu (RoomSuggestion.svelte) using a ~ char trigger, making use of roomsByUrl and userSpaceUrls.
  • Text Parsing & Rendering:
    • Implemented custom parsing logic (lib/content-text.ts) that correctly pulls apart wss:// text sequences into relay, room, or text types, elegantly separating trailing punctuations.
    • Upgraded Content.svelte and ContentMinimal.svelte to route valid parsed segments into ContentText.svelte.
    • Added native clickable rendering for room instances (navigating to the specific room) and relay instances (navigating to the overarching space).
  • Tooling:
    • Brought in vitest to support rigorous module testing.
    • Added test coverage in tests/content-text.test.ts to validate proper regex extraction of relays and room combinations.

Validation

  • Type checking (pnpm run check) and linter pass seamlessly.
  • Tested vitest run on parseContentTextParts ensuring trailing commas/parentheses don't break clickable references.
  • Confirmed ~ successfully initiates the mention menu with responsive rendering.
## Summary This PR introduces first-class support for Room and Space mentions (Issue #130), mirroring the existing pubkey mentions behavior. It adds full rich-text editor integration via Tiptap, content parsing for display, and supporting unit tests. Users can now type `~` anywhere in the compose menu to trigger a room suggestion search overlay, which securely references NIP-29 rooms using the `relay_url'room_id` format and renders appropriately across the application. Resolves #130. ## Changes Included - **Tiptap Editor Integration**: - Added `RoomReferenceExtension` and `RoomReferenceNodeView` to safely encode room mentions into the `relay_url'room_id` reference format. - Implemented `roomReferenceSearch` in `editor/index.ts` to power the suggestion menu (`RoomSuggestion.svelte`) using a `~` char trigger, making use of `roomsByUrl` and `userSpaceUrls`. - **Text Parsing & Rendering**: - Implemented custom parsing logic (`lib/content-text.ts`) that correctly pulls apart `wss://` text sequences into `relay`, `room`, or `text` types, elegantly separating trailing punctuations. - Upgraded `Content.svelte` and `ContentMinimal.svelte` to route valid parsed segments into `ContentText.svelte`. - Added native clickable rendering for `room` instances (navigating to the specific room) and `relay` instances (navigating to the overarching space). - **Tooling**: - Brought in `vitest` to support rigorous module testing. - Added test coverage in `tests/content-text.test.ts` to validate proper regex extraction of relays and room combinations. ## Validation - Type checking (`pnpm run check`) and linter pass seamlessly. - Tested `vitest run` on `parseContentTextParts` ensuring trailing commas/parentheses don't break clickable references. - Confirmed `~` successfully initiates the mention menu with responsive rendering.
Owner

This looks quite good, the only thing I noticed is the rendering code never gets called because the room identifier includes a relay, which in turn gets parsed as a url. Which is actually nice, because you can remove the ContentText component and parser entirely and move the rendering logic into the various ContentLink* components

This looks quite good, the only thing I noticed is the rendering code never gets called because the room identifier includes a relay, which in turn gets parsed as a url. Which is actually nice, because you can remove the ContentText component and parser entirely and move the rendering logic into the various ContentLink* components
Author
Contributor

@hodlbod Should i keep that as a feature? Or try to implement a fix?

@hodlbod Should i keep that as a feature? Or try to implement a fix?
Owner

Yes, please go ahead and remove the rendering changes and just make the adjustments to the link renderer.

Yes, please go ahead and remove the rendering changes and just make the adjustments to the link renderer.
hodlbod requested changes 2026-04-06 19:38:58 +00:00
@@ -17,0 +31,4 @@
}
return {url: normalizeRelayUrl(roomUrl), h}
}
Owner

I think we can live dangerously here by adding isRoomId to core/state which just checks for an apostrophe, and use splitRoomId from that same file.

I think we can live dangerously here by adding `isRoomId` to core/state which just checks for an apostrophe, and use `splitRoomId` from that same file.
Khushvendra marked this conversation as resolved
@@ -52,0 +74,4 @@
{:else if roomReference || relayReference}
<div class="bg-alt p-4 leading-normal">
<Icon icon={LinkRound} size={3} class="inline-block" />
<span class="ml-2">{displayUrl(url)}</span>
Owner

We should display url/h more helpfully. In either case, prefix with a tilde. Display the url as the space name, and if an h is included append it after the slash, like this: ~Coracle Spaces / Design. Make the same change to the edit interface as well.

We should display url/h more helpfully. In either case, prefix with a tilde. Display the url as the space name, and if an `h` is included append it after the slash, like this: `~Coracle Spaces / Design`. Make the same change to the edit interface as well.
Khushvendra marked this conversation as resolved
@@ -0,0 +13,4 @@
<div class="flex max-w-full flex-col gap-1">
<div class="overflow-hidden text-ellipsis text-base font-semibold">~{$room.name || h}</div>
<div class="overflow-hidden text-ellipsis text-sm opacity-75">{displayRelayUrl(url)}'{h}</div>
Owner

This should be displayed the same way we'll ultimately render it, as ~Space Name / Room Name

This should be displayed the same way we'll ultimately render it, as `~Space Name / Room Name`
Khushvendra marked this conversation as resolved
@@ -85,0 +102,4 @@
name: room.name || "",
h: room.h,
url: roomUrl,
})
Owner

I think we can safely use RoomMeta as the type here rather than using an ad-hoc one

I think we can safely use `RoomMeta` as the type here rather than using an ad-hoc one
Khushvendra marked this conversation as resolved
Owner

Please go ahead and rebase on dev to fix conflicts.

Please go ahead and rebase on dev to fix conflicts.
Owner

Looks like some new conflicts have cropped up, can you rebase and fix those too?

Looks like some new conflicts have cropped up, can you rebase and fix those too?
Khushvendra added 3 commits 2026-04-08 17:02:21 +00:00
Khushvendra force-pushed feat/room-mentions-130 from 0c40be9cfc to 540e9abe3d 2026-04-08 17:02:21 +00:00 Compare
hodlbod reviewed 2026-04-08 17:17:35 +00:00
@@ -16,0 +54,4 @@
}
return displayUrl(url)
})
Owner

Let's refactor this a bit. Go ahead and add a new component called ContentLinkUrl that encapsulates this logic and is used in both inline and block contexts. In the block case you'll still need to detect whether the url is a room or relay, but that should be as simple as isRoomId(url) || isRelayUrl(url)

Let's refactor this a bit. Go ahead and add a new component called `ContentLinkUrl` that encapsulates this logic and is used in both inline and block contexts. In the block case you'll still need to detect whether the url is a room or relay, but that should be as simple as `isRoomId(url) || isRelayUrl(url)`
Khushvendra marked this conversation as resolved
hodlbod reviewed 2026-04-08 17:17:52 +00:00
hodlbod left a comment
Owner

Looks pretty good, just one comment.

Looks pretty good, just one comment.
Khushvendra added 1 commit 2026-04-08 18:42:13 +00:00
hodlbod reviewed 2026-04-09 19:35:42 +00:00
hodlbod left a comment
Owner

One small change

One small change
@@ -90,0 +56,4 @@
showIcon
labelClass="ml-2" />
{:else}
<ContentLinkUrl {url} class="my-2 block">
Owner

It doesn't make sense to use ContentLinkUrl in this case since because we're providing our own content, it's doing basically nothing. Remove the children and showIcon props from ContentLinkUrl. You can might be able to remove the labelClass as well, not sure.

It doesn't make sense to use ContentLinkUrl in this case since because we're providing our own content, it's doing basically nothing. Remove the children and showIcon props from ContentLinkUrl. You can might be able to remove the labelClass as well, not sure.
hodlbod marked this conversation as resolved
Khushvendra added 1 commit 2026-04-11 13:27:04 +00:00
Owner

Merged via ec0b6a99

Merged via ec0b6a99
hodlbod closed this pull request 2026-04-11 17:13:44 +00:00

Pull request closed

Sign in to join this conversation.