[research] "Copy Link" should open in Lotus for guild members — matrix.to client hints vs direct chat.lotusguild.org permalinks #130

Closed
opened 2026-09-17 16:10:05 -04:00 by jared · 2 comments
Owner

Today Message.tsx:400 copies getMatrixToRoomEvent(...) — a https://matrix.to/#/… link. For guild members that lands on the matrix.to chooser page instead of opening Lotus.

Research

  1. matrix.to: does it support a client hint / preferred-client redirect (check the matrix.to source — client= / "open in" memory, and whether custom (non-listed) clients can be registered)? Also the matrix: URI scheme (MSC2312) and whether the desktop app's existing matrix: deep-link registration means a matrix: link is the better "opens in Lotus" form on desktop.
  2. Direct permalinks: https://chat.lotusguild.org/#/room/<roomId>/<eventId>?via=… — confirm the router already handles them (it should — the app navigates by hash) and what a non-member / other-homeserver user sees.
  3. Proposal: Copy Link → Lotus permalink (opens in the PWA/desktop for guild members); Copy matrix.to Link as a second item for sharing outside. Same for room links in RoomNavItem. Config-driven base URL (config.json hostBaseUrl) — no hardcoding.
Today `Message.tsx:400` copies `getMatrixToRoomEvent(...)` — a `https://matrix.to/#/…` link. For guild members that lands on the matrix.to chooser page instead of opening Lotus. ### Research 1. matrix.to: does it support a client hint / preferred-client redirect (check the matrix.to source — `client=` / "open in" memory, and whether custom (non-listed) clients can be registered)? Also the `matrix:` URI scheme (MSC2312) and whether the desktop app's existing `matrix:` deep-link registration means a `matrix:` link is the better "opens in Lotus" form on desktop. 2. Direct permalinks: `https://chat.lotusguild.org/#/room/<roomId>/<eventId>?via=…` — confirm the router already handles them (it should — the app navigates by hash) and what a non-member / other-homeserver user sees. 3. Proposal: **Copy Link** → Lotus permalink (opens in the PWA/desktop for guild members); **Copy matrix.to Link** as a second item for sharing outside. Same for room links in `RoomNavItem`. Config-driven base URL (`config.json` `hostBaseUrl`) — no hardcoding.
jared added this to the Features 2026-Q4 milestone 2026-09-17 16:10:05 -04:00
jared added the enhancementpriority: mediumarea: messagingresearch labels 2026-09-17 16:10:05 -04:00
jared self-assigned this 2026-09-17 16:10:05 -04:00
Author
Owner

Research findings (2026-09-17)

What Lotus produces today

Four places copy a link, all through src/app/plugins/matrix-to.ts and all https://matrix.to/#/…:

Surface Helper Output
Message ⋯ → Copy Link (Message.tsx:389-414) getMatrixToRoomEvent(roomId, eventId, via) https://matrix.to/#/!room:server/$event?via=…
Space header / space tab → Copy Link (Space.tsx:124, SpaceTabs.tsx:137) getMatrixToRoom(idOrAlias, via) https://matrix.to/#/#alias:server
Room Settings → Share Room (link + local QR) (InviteUserPrompt.tsx:80) getMatrixToRoom same

Links clicked inside Lotus never leave the app: react-custom-html-parser.tsx:178-202 parses matrix.to room/event links and navigates in place, and on Windows desktop useDeepLinkNavigate.ts receives OS-level matrix: URIs (MSC2312) forwarded by Tauri. So the problem is only the link a member pastes into Discord / SMS / email, which a browser opens on matrix.to.

What matrix.to can and cannot do (read the source, matrix-org/matrix.to@main)

  • Cinny is a known client (src/open/clients/Cinny.js, id cinny), so ?client=cinny is valid and matrix.to will show "Continue in Cinny"… but its deep link is hard-coded to https://app.cinny.in/home/<room>/<event>. There is no way to point it at chat.lotusguild.org.
  • web-instance[<client>]=<host> exists but only Element honours it, and only against a hard-coded allowlist (trustedWebInstances in Element.js: app.element.io, develop.element.io, chat.mozilla.org, chat.fedoraproject.org, chat.fosdem.org, webchat.kde.org, app.gitter.im, chat.blender.org). Cinny.js has getPreferredWebInstance() empty. ?web-instance[cinny]=chat.lotusguild.org is parsed (Link.js doesn't validate it) and then ignored.
  • Client preference is per-browser localStorage on matrix.to ("remember my choice"); a link cannot set it.
  • matrix.to also offers the raw matrix: URI ("open in your default app"). Lotus desktop already registers that scheme on Windows, so desktop members can pick it — but it's a manual step on every device, and web/PWA users have no default handler.
  • The matrix.to Cinny adapter appends ?via=a,b, while Lotus/Cinny's router reads ?viaServers=a,b (paths.ts:48-51), so via servers from matrix.to → Cinny links are silently dropped today. (Upstream Cinny bug; irrelevant for joined rooms, matters for previews of unjoined federated rooms.)

Upstream route: a PR to matrix.to adding getPreferredWebInstance + a trustedWebInstances list to Cinny.js (mirroring Element's) with chat.lotusguild.org in it would make ?client=cinny&web-instance[cinny]=chat.lotusguild.org work. Mozilla/Fedora/KDE got in that way. Realistic but slow (matrix.to is lightly maintained), and it still only helps once the recipient clicks "Continue in Cinny"; matrix.to's remembered choice for most people is Element.

Direct permalinks — verified working today

https://chat.lotusguild.org/home/<encoded room id or alias>/<encoded event id>?viaServers=a,b is already a fully functional deep link:

  • Prod nginx serves SPA paths (curl -I https://chat.lotusguild.org/home/%21abc%3A…200); hashRouter.enabled is false so no #/.
  • Cold load while logged out: the index loader stores the path via setAfterLoginRedirectPath (Router.tsx:113-118) and the user lands in the room after login. Logged in: the route resolves; an unjoined room goes through the existing join-before-navigate preview (RoomCard, knock/join).
  • Non-Lotus recipients get the Lotus login page with the homeserver preselected — for anyone on another homeserver that is a dead end unless they log in to Lotus with their own account (allowCustomHomeservers: true, so they can, but it is not what they expect from a "Matrix link").

Recommendation

Copy two kinds of link, chosen by where it will be pasted, not one link that tries to be both:

  1. Default (unchanged): matrix.to, but append ?client=cinny only after the upstream web-instance[cinny] PR lands — until then the parameter just pushes people to app.cinny.in, which is worse than the picker. Keep matrix.to as the interoperable link; it is what the ecosystem expects and what other clients unfurl.
  2. Add a second item, "Copy Lotus Link", next to every "Copy Link" (message menu, space menu, Share Room), producing the direct https://chat.lotusguild.org/… permalink. Label it plainly; the tooltip says "Opens straight in Lotus Chat — for guild members". No setting, no auto-detection of the recipient (we can't know where it's going).
    • Build it from the existing path helpers (getHomeRoomPath / getSpaceRoomPath + withSearchParam({ viaServers })) prefixed with getOriginBaseUrl(), so it is correct for any deployment (dev, LXC 106, desktop) and for the hash-router config if it is ever turned on. ~60 lines: one getLotusPermalink(room, eventId?) helper in plugins/matrix-to.ts (or utils/permalink.ts) + 3 menu items.
    • Space links should use the alias when the space has a canonical alias (readable, survives room upgrades better than an id).
    • Make Lotus recognise its own permalinks on click the same way it recognises matrix.to (react-custom-html-parser.tsx:178): a chat.lotusguild.org/home/… link pasted back into a room should navigate in place, not open a new tab. That is the one piece of new parsing (parseLotusPermalink(href) matching getOriginBaseUrl()), and it should also feed useDeepLinkNavigate so the desktop app handles https://chat.lotusguild.org/… opened from outside via Tauri's deep-link plugin (needs the https scheme registered in tauri.conf.json — cinny-desktop follow-up, not required for v1).
    • Do not rewrite outgoing message bodies or the QR code: the Share Room QR keeps matrix.to (a phone camera's Matrix app expects it).
  3. Separately and cheaply: fix the via vs viaServers mismatch by accepting both names in useSearchParamsViaServers, so matrix.to → "Continue in Cinny" links carry their via servers. Also worth a one-line upstream PR to Cinny.
  4. Optional, later: open the matrix.to PR (Cinny.js web-instance allowlist with chat.lotusguild.org). Low effort, uncertain timeline; it would make option 1's ?client=cinny&web-instance[cinny]=chat.lotusguild.org the best-of-both link.

Nothing here touches RoomTimeline or send paths; risk is confined to three menus and one link parser.

## Research findings (2026-09-17) ### What Lotus produces today Four places copy a link, all through `src/app/plugins/matrix-to.ts` and all `https://matrix.to/#/…`: | Surface | Helper | Output | | :-- | :-- | :-- | | Message ⋯ → **Copy Link** (`Message.tsx:389-414`) | `getMatrixToRoomEvent(roomId, eventId, via)` | `https://matrix.to/#/!room:server/$event?via=…` | | Space header / space tab → **Copy Link** (`Space.tsx:124`, `SpaceTabs.tsx:137`) | `getMatrixToRoom(idOrAlias, via)` | `https://matrix.to/#/#alias:server` | | Room Settings → Share Room (link + local QR) (`InviteUserPrompt.tsx:80`) | `getMatrixToRoom` | same | Links *clicked inside* Lotus never leave the app: `react-custom-html-parser.tsx:178-202` parses matrix.to room/event links and navigates in place, and on Windows desktop `useDeepLinkNavigate.ts` receives OS-level `matrix:` URIs (MSC2312) forwarded by Tauri. So the problem is only the link a member pastes into Discord / SMS / email, which a browser opens on matrix.to. ### What matrix.to can and cannot do (read the source, `matrix-org/matrix.to@main`) - **Cinny is a known client** (`src/open/clients/Cinny.js`, id `cinny`), so `?client=cinny` is valid and matrix.to will show "Continue in Cinny"… but its deep link is **hard-coded to `https://app.cinny.in/home/<room>/<event>`**. There is no way to point it at `chat.lotusguild.org`. - **`web-instance[<client>]=<host>` exists but only Element honours it**, and only against a hard-coded allowlist (`trustedWebInstances` in `Element.js`: app.element.io, develop.element.io, chat.mozilla.org, chat.fedoraproject.org, chat.fosdem.org, webchat.kde.org, app.gitter.im, chat.blender.org). `Cinny.js` has `getPreferredWebInstance()` empty. `?web-instance[cinny]=chat.lotusguild.org` is parsed (`Link.js` doesn't validate it) and then ignored. - **Client preference is per-browser localStorage on matrix.to** ("remember my choice"); a link cannot set it. - matrix.to also offers the raw **`matrix:` URI** ("open in your default app"). Lotus desktop already registers that scheme on Windows, so desktop members can pick it — but it's a manual step on every device, and web/PWA users have no default handler. - The matrix.to Cinny adapter appends `?via=a,b`, while Lotus/Cinny's router reads `?viaServers=a,b` (`paths.ts:48-51`), so via servers from matrix.to → Cinny links are silently dropped today. (Upstream Cinny bug; irrelevant for joined rooms, matters for previews of unjoined federated rooms.) **Upstream route:** a PR to matrix.to adding `getPreferredWebInstance` + a `trustedWebInstances` list to `Cinny.js` (mirroring Element's) with `chat.lotusguild.org` in it would make `?client=cinny&web-instance[cinny]=chat.lotusguild.org` work. Mozilla/Fedora/KDE got in that way. Realistic but slow (matrix.to is lightly maintained), and it still only helps once the recipient clicks "Continue in Cinny"; matrix.to's remembered choice for most people is Element. ### Direct permalinks — verified working today `https://chat.lotusguild.org/home/<encoded room id or alias>/<encoded event id>?viaServers=a,b` is already a fully functional deep link: - Prod nginx serves SPA paths (`curl -I https://chat.lotusguild.org/home/%21abc%3A…` → **200**); `hashRouter.enabled` is `false` so no `#/`. - Cold load while logged out: the index loader stores the path via `setAfterLoginRedirectPath` (`Router.tsx:113-118`) and the user lands in the room after login. Logged in: the route resolves; an unjoined room goes through the existing `join-before-navigate` preview (`RoomCard`, knock/join). - Non-Lotus recipients get the Lotus login page with the homeserver preselected — for anyone on another homeserver that is a dead end unless they log in to Lotus with their own account (`allowCustomHomeservers: true`, so they can, but it is not what they expect from a "Matrix link"). ### Recommendation Copy **two kinds of link, chosen by where it will be pasted**, not one link that tries to be both: 1. **Default (unchanged): matrix.to**, but append `?client=cinny` **only after** the upstream `web-instance[cinny]` PR lands — until then the parameter just pushes people to app.cinny.in, which is worse than the picker. Keep matrix.to as the interoperable link; it is what the ecosystem expects and what other clients unfurl. 2. **Add a second item, "Copy Lotus Link"**, next to every "Copy Link" (message menu, space menu, Share Room), producing the direct `https://chat.lotusguild.org/…` permalink. Label it plainly; the tooltip says "Opens straight in Lotus Chat — for guild members". No setting, no auto-detection of the recipient (we can't know where it's going). - Build it from the existing path helpers (`getHomeRoomPath` / `getSpaceRoomPath` + `withSearchParam({ viaServers })`) prefixed with `getOriginBaseUrl()`, so it is correct for any deployment (dev, LXC 106, desktop) and for the hash-router config if it is ever turned on. ~60 lines: one `getLotusPermalink(room, eventId?)` helper in `plugins/matrix-to.ts` (or `utils/permalink.ts`) + 3 menu items. - Space links should use the alias when the space has a canonical alias (readable, survives room upgrades better than an id). - Make Lotus **recognise its own permalinks** on click the same way it recognises matrix.to (`react-custom-html-parser.tsx:178`): a `chat.lotusguild.org/home/…` link pasted back into a room should navigate in place, not open a new tab. That is the one piece of new parsing (`parseLotusPermalink(href)` matching `getOriginBaseUrl()`), and it should also feed `useDeepLinkNavigate` so the desktop app handles `https://chat.lotusguild.org/…` opened from outside via Tauri's deep-link plugin (needs the https scheme registered in `tauri.conf.json` — cinny-desktop follow-up, not required for v1). - Do **not** rewrite outgoing message bodies or the QR code: the Share Room QR keeps matrix.to (a phone camera's Matrix app expects it). 3. Separately and cheaply: fix the `via` vs `viaServers` mismatch by accepting both names in `useSearchParamsViaServers`, so matrix.to → "Continue in Cinny" links carry their via servers. Also worth a one-line upstream PR to Cinny. 4. Optional, later: open the matrix.to PR (`Cinny.js` web-instance allowlist with `chat.lotusguild.org`). Low effort, uncertain timeline; it would make option 1's `?client=cinny&web-instance[cinny]=chat.lotusguild.org` the best-of-both link. Nothing here touches `RoomTimeline` or send paths; risk is confined to three menus and one link parser.
Author
Owner

Built and verified — f528e5e4

Facts from the raw sources (not summaries): matrix.to/src/open/clients/Cinny.js hard-codes https://app.cinny.in/ and emits ?via=a,b; Element.js is the only adapter honouring web-instance[], against a hard-coded allowlist; upstream Cinny paths.ts reads viaServers. So matrix.to cannot be steered here, and the recommendation stands: keep matrix.to as the default link, add a direct one.

Shipped

  • Copy Lotus Link next to Copy Link in the message ⋯ menu, the space header menu and the sidebar space-tab menu (plugins/lotus-permalink.ts, unit-tested).
  • Lotus links pasted into a room render as room/event mentions and navigate in place (rewritten to the matrix.to form inside the HTML parser — no second pipeline).
  • /home/<room> for a joined room that lives under a space or in Direct now redirects to its real route instead of the preview card + "View" button. This is also the form matrix.to → "Continue in Cinny" produces, so those links improve too.
  • ?via=a,b accepted as an alias of ?viaServers= (the matrix.to Cinny adapter's form).
  • Bug found on the way, fixed: a deep link opened while logged out was dropped after an OIDC/SSO login. OidcCallback reloads at the app root (window.location.replace(BASE_URL)), and the index loader then went straight to /home, ignoring the stored after_login_redirect_url that the password flow honours. The index loader now consumes it via the shared takeAfterLoginPath(). This affected every guild member (SSO).

Verified with Playwright on a local Synapse (space containing a 400-message room, both users joined):

  1. Logged out → open /home/<room>/<event> → login page → log in → lands on /<space>/<room>/<event> with the target message on screen.
  2. Message menu → Copy Lotus Link → clipboard holds <base>/home/<room>/<event>?viaServers=localhost.
  3. That link posted by the other user renders as Message: #Busy Room; clicking it jumps to the message in the same tab (no new page), URL carries the event id.
  4. Space header menu and space tab menu both copy <base>/<space>?viaServers=localhost.

The OIDC redirect itself could not be exercised locally (no MAS); the change is a one-line reuse of the password flow's helper in the index route, covered by reading both paths.

Not done / optional follow-ups: Share Room dialog and its QR still use matrix.to only (by design — a phone camera's Matrix app expects it); desktop app does not register https://chat.lotusguild.org as a deep-link scheme (Tauri deep-link plugin config in cinny-desktop; web handling is already there); upstream PR to matrix.to adding getPreferredWebInstance + allowlist to Cinny.js — worth opening if you want ?client=cinny&web-instance[cinny]=chat.lotusguild.org to work one day.

## Built and verified — `f528e5e4` Facts from the raw sources (not summaries): `matrix.to/src/open/clients/Cinny.js` hard-codes `https://app.cinny.in/` and emits `?via=a,b`; `Element.js` is the only adapter honouring `web-instance[]`, against a hard-coded allowlist; upstream Cinny `paths.ts` reads `viaServers`. So matrix.to cannot be steered here, and the recommendation stands: keep matrix.to as the default link, add a direct one. **Shipped** - **Copy Lotus Link** next to Copy Link in the message ⋯ menu, the space header menu and the sidebar space-tab menu (`plugins/lotus-permalink.ts`, unit-tested). - Lotus links pasted into a room render as room/event mentions and navigate in place (rewritten to the matrix.to form inside the HTML parser — no second pipeline). - `/home/<room>` for a joined room that lives under a space or in Direct now **redirects to its real route** instead of the preview card + "View" button. This is also the form matrix.to → "Continue in Cinny" produces, so those links improve too. - `?via=a,b` accepted as an alias of `?viaServers=` (the matrix.to Cinny adapter's form). - **Bug found on the way, fixed:** a deep link opened while logged out was dropped after an **OIDC/SSO** login. `OidcCallback` reloads at the app root (`window.location.replace(BASE_URL)`), and the index loader then went straight to `/home`, ignoring the stored `after_login_redirect_url` that the password flow honours. The index loader now consumes it via the shared `takeAfterLoginPath()`. This affected every guild member (SSO). **Verified with Playwright on a local Synapse** (space containing a 400-message room, both users joined): 1. Logged out → open `/home/<room>/<event>` → login page → log in → lands on `/<space>/<room>/<event>` with the target message on screen. 2. Message menu → Copy Lotus Link → clipboard holds `<base>/home/<room>/<event>?viaServers=localhost`. 3. That link posted by the other user renders as `Message: #Busy Room`; clicking it jumps to the message in the same tab (no new page), URL carries the event id. 4. Space header menu and space tab menu both copy `<base>/<space>?viaServers=localhost`. The OIDC redirect itself could not be exercised locally (no MAS); the change is a one-line reuse of the password flow's helper in the index route, covered by reading both paths. **Not done / optional follow-ups:** Share Room dialog and its QR still use matrix.to only (by design — a phone camera's Matrix app expects it); desktop app does not register `https://chat.lotusguild.org` as a deep-link scheme (Tauri deep-link plugin config in cinny-desktop; web handling is already there); upstream PR to matrix.to adding `getPreferredWebInstance` + allowlist to `Cinny.js` — worth opening if you want `?client=cinny&web-instance[cinny]=chat.lotusguild.org` to work one day.
jared closed this issue 2026-09-18 00:19:36 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#130