diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 8a5cb3426..129237f5f 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -83,8 +83,6 @@ import { copyToClipboard } from '../../../utils/dom'; import { stopPropagation } from '../../../utils/keyboard'; import { getMatrixToRoomEvent } from '../../../plugins/matrix-to'; import { getLotusRoomPermalink } from '../../../plugins/lotus-permalink'; -import { getOriginBaseUrl } from '../../../pages/pathUtils'; -import { useClientConfig } from '../../../hooks/useClientConfig'; import { getViaServers } from '../../../plugins/via-servers'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents'; @@ -98,6 +96,7 @@ import { useLongPress } from '../../../hooks/useLongPress'; import { ActionSheet } from '../../../components/action-sheet'; import { useBookmarks } from '../../../hooks/useBookmarks'; import { PresenceRingAvatar } from '../../../components/presence'; +import { useLotusShareBase } from '../../../hooks/useLotusLinkBase'; import { AvatarDecoration } from '../../../components/avatar-decoration/AvatarDecoration'; // Delivery status indicator for own messages @@ -460,18 +459,11 @@ export const MessageCopyLotusLinkItem = as< onClose?: () => void; } >(({ room, mEvent, onClose, ...props }, ref) => { - const { hashRouter } = useClientConfig(); + const lotusBase = useLotusShareBase(); const handleCopy = () => { const eventId = mEvent.getId(); if (!eventId) return; - copyToClipboard( - getLotusRoomPermalink( - getOriginBaseUrl(hashRouter), - room.roomId, - eventId, - getViaServers(room), - ), - ); + copyToClipboard(getLotusRoomPermalink(lotusBase, room.roomId, eventId, getViaServers(room))); onClose?.(); }; diff --git a/src/app/hooks/useLotusLinkBase.test.ts b/src/app/hooks/useLotusLinkBase.test.ts new file mode 100644 index 000000000..76efdd64a --- /dev/null +++ b/src/app/hooks/useLotusLinkBase.test.ts @@ -0,0 +1,23 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { lotusLinkBases, lotusShareBase } from './useLotusLinkBase'; + +const LOCAL = 'http://localhost:44548/#/'; +const WEB = 'https://chat.lotusguild.org'; + +test('web: shared links use the page origin', () => { + assert.equal(lotusShareBase(WEB, false, WEB), WEB); + assert.deepEqual(lotusLinkBases(WEB, false, undefined), [WEB]); +}); + +test('desktop with webAppUrl: shared links use the public web app', () => { + assert.equal(lotusShareBase(LOCAL, true, `${WEB}/`), WEB); + assert.deepEqual(lotusLinkBases(LOCAL, true, WEB), [LOCAL, WEB]); +}); + +test('desktop without a valid webAppUrl falls back to the origin', () => { + [undefined, '', 'http://chat.lotusguild.org', 'nonsense'].forEach((v) => { + assert.equal(lotusShareBase(LOCAL, true, v), LOCAL, String(v)); + assert.deepEqual(lotusLinkBases(LOCAL, true, v), [LOCAL]); + }); +}); diff --git a/src/app/hooks/useLotusLinkBase.ts b/src/app/hooks/useLotusLinkBase.ts new file mode 100644 index 000000000..b072ad9af --- /dev/null +++ b/src/app/hooks/useLotusLinkBase.ts @@ -0,0 +1,37 @@ +import { useMemo } from 'react'; +import { useClientConfig } from './useClientConfig'; +import { isTauri } from './useTauri'; +import { getOriginBaseUrl } from '../pages/pathUtils'; +import { resolveWebAppUrl } from '../utils/callInBrowser'; + +/** + * [Gitea #248] Base URL for Lotus links meant for other people. + * + * On the web that's this page's origin. In the desktop app the page is served + * from a local address (and hash-routed), which nobody else can open, so use + * the public web app from config `webAppUrl` (web path routing) instead. + * Without a valid `webAppUrl` it falls back to the origin, as before. + */ +export const lotusShareBase = (originBase: string, desktop: boolean, webAppUrl: unknown): string => + (desktop && resolveWebAppUrl(webAppUrl)) || originBase; + +/** + * Every base a Lotus link to this deployment may start with: the origin, plus + * the public web app in the desktop app, so a shared https link opens in-app. + */ +export const lotusLinkBases = ( + originBase: string, + desktop: boolean, + webAppUrl: unknown, +): string[] => { + const shared = lotusShareBase(originBase, desktop, webAppUrl); + return shared === originBase ? [originBase] : [originBase, shared]; +}; + +export const useLotusShareBase = (): string => { + const { hashRouter, webAppUrl } = useClientConfig(); + return useMemo( + () => lotusShareBase(getOriginBaseUrl(hashRouter), isTauri(), webAppUrl), + [hashRouter, webAppUrl], + ); +}; diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx index a2a7d7ddd..95ea1bc44 100644 --- a/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -77,6 +77,7 @@ import { KeyboardShortcutsDialog, useKeyboardShortcutsTrigger } from '../../feat import { useRoomsListener } from '../../hooks/useRoomsListener'; import { threadNotificationsAtom } from '../../state/threadNotifications'; import { roomIdToActiveThreadIdAtomFamily } from '../../state/room/thread'; +import { lotusLinkBases } from '../../hooks/useLotusLinkBase'; import { getThreadNotificationMode, shouldNotifyThreadReply, @@ -118,11 +119,12 @@ function SystemEmojiFeature() { // [Gitea #103] Mirror the privacy toggle into the html parser's module flag. function LotusPermalinkFeature() { - const { hashRouter } = useClientConfig(); + const { hashRouter, webAppUrl } = useClientConfig(); useEffect(() => { - setLotusPermalinkBase(getOriginBaseUrl(hashRouter)); + // [Gitea #248] In the desktop app, links to the public web app count too. + setLotusPermalinkBase(lotusLinkBases(getOriginBaseUrl(hashRouter), isTauriApp(), webAppUrl)); return () => setLotusPermalinkBase(undefined); - }, [hashRouter]); + }, [hashRouter, webAppUrl]); return null; } diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 601caa3d2..f4bb05b34 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -49,13 +49,7 @@ import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { allRoomsAtom } from '../../../state/room-list/roomList'; import { useAnyRoomLiveCall } from '../../../hooks/useSpaceLiveCall'; import { LiveDot } from './SpaceTabs.css'; -import { - getOriginBaseUrl, - getSpaceLobbyPath, - getSpacePath, - joinPathComponent, -} from '../../pathUtils'; -import { useClientConfig } from '../../../hooks/useClientConfig'; +import { getSpaceLobbyPath, getSpacePath, joinPathComponent } from '../../pathUtils'; import { SidebarAvatar, SidebarItem, @@ -102,6 +96,7 @@ import { settingsAtom } from '../../../state/settings'; import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings'; import { useRoomCreators } from '../../../hooks/useRoomCreators'; import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; +import { useLotusShareBase } from '../../../hooks/useLotusLinkBase'; import { InviteUserPrompt } from '../../../components/invite-user-prompt'; type SpaceMenuProps = { @@ -112,7 +107,7 @@ type SpaceMenuProps = { const SpaceMenu = forwardRef( ({ room, requestClose, onUnpin }, ref) => { const mx = useMatrixClient(); - const { hashRouter } = useClientConfig(); + const lotusBase = useLotusShareBase(); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const roomToParents = useAtomValue(roomToParentsAtom); const powerLevels = usePowerLevels(room); @@ -152,9 +147,7 @@ const SpaceMenu = forwardRef( const handleCopyLotusLink = () => { const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId); const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room); - copyToClipboard( - getLotusSpacePermalink(getOriginBaseUrl(hashRouter), roomIdOrAlias, viaServers), - ); + copyToClipboard(getLotusSpacePermalink(lotusBase, roomIdOrAlias, viaServers)); requestClose(); }; diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index 336c55eec..f96c29f37 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -39,13 +39,7 @@ import { NavItemContent, NavLink, } from '../../../components/nav'; -import { - getOriginBaseUrl, - getSpaceLobbyPath, - getSpaceRoomPath, - getSpaceSearchPath, -} from '../../pathUtils'; -import { useClientConfig } from '../../../hooks/useClientConfig'; +import { getSpaceLobbyPath, getSpaceRoomPath, getSpaceSearchPath } from '../../pathUtils'; import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { @@ -92,6 +86,7 @@ import { ContainerColor } from '../../../styles/ContainerColor.css'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { BreakWord } from '../../../styles/Text.css'; import { InviteUserPrompt } from '../../../components/invite-user-prompt'; +import { useLotusShareBase } from '../../../hooks/useLotusLinkBase'; import { useCallEmbed } from '../../../hooks/useCallEmbed'; type SpaceMenuProps = { @@ -100,7 +95,7 @@ type SpaceMenuProps = { }; const SpaceMenu = forwardRef(({ room, requestClose }, ref) => { const mx = useMatrixClient(); - const { hashRouter } = useClientConfig(); + const lotusBase = useLotusShareBase(); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [developerTools] = useSetting(settingsAtom, 'developerTools'); const roomToParents = useAtomValue(roomToParentsAtom); @@ -137,9 +132,7 @@ const SpaceMenu = forwardRef(({ room, requestClo const handleCopyLotusLink = () => { const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId); const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room); - copyToClipboard( - getLotusSpacePermalink(getOriginBaseUrl(hashRouter), roomIdOrAlias, viaServers), - ); + copyToClipboard(getLotusSpacePermalink(lotusBase, roomIdOrAlias, viaServers)); requestClose(); }; diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx index 340802353..7725e65b0 100644 --- a/src/app/plugins/react-custom-html-parser.tsx +++ b/src/app/plugins/react-custom-html-parser.tsx @@ -122,11 +122,15 @@ const cleanHref = (href: string): string => stripTrackingOnRender ? stripTrackingParams(href) : href; // [Gitea #130] Links to THIS deployment's room routes render and click like -// matrix.to links. The base is set once from the client config -// (ClientNonUIFeatures) because this module has no access to hooks. -let lotusPermalinkBase: string | undefined; -export const setLotusPermalinkBase = (baseUrl: string | undefined): void => { - lotusPermalinkBase = baseUrl; +// matrix.to links. The bases are set once from the client config +// (ClientNonUIFeatures) because this module has no access to hooks. [Gitea +// #248] Several: the desktop app knows both its local origin and the public web +// app (`webAppUrl`), and people share the public one. +let lotusPermalinkBases: string[] = []; +export const setLotusPermalinkBase = (baseUrls: string | string[] | undefined): void => { + lotusPermalinkBases = (Array.isArray(baseUrls) ? baseUrls : [baseUrls]).filter( + (b): b is string => !!b, + ); }; /** * The matrix.to form of `href` when it is a matrix.to link or a Lotus @@ -134,7 +138,11 @@ export const setLotusPermalinkBase = (baseUrl: string | undefined): void => { */ export const toMatrixToHref = (href: string): string | undefined => { if (testMatrixTo(href)) return href; - return lotusPermalinkBase ? lotusPermalinkToMatrixTo(lotusPermalinkBase, href) : undefined; + for (const base of lotusPermalinkBases) { + const matrixTo = lotusPermalinkToMatrixTo(base, href); + if (matrixTo) return matrixTo; + } + return undefined; }; export const LINKIFY_OPTS: LinkifyOpts = {