From 5058136737e177a5cabecc8b53b489eaabc23429 Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Tue, 30 Jul 2024 17:48:59 +0530
Subject: [PATCH] support matrix.to links (#1849)
* support room via server params and eventId
* change copy link to matrix.to links
* display matrix.to links in messages as pill and stop generating url previews for them
* improve editor mention to include viaServers and eventId
* fix mention custom attributes
* always try to open room in current space
* jump to latest remove target eventId from url
* add create direct search options to open/create dm with url
---
src/app/components/RenderMessageContent.tsx | 55 ++--
.../autocomplete/RoomMentionAutocomplete.tsx | 7 +-
src/app/components/editor/input.ts | 37 ++-
src/app/components/editor/output.ts | 17 +-
src/app/components/editor/slate.d.ts | 2 +
src/app/components/editor/utils.ts | 6 +-
src/app/components/message/RenderBody.tsx | 11 +-
src/app/components/room-card/RoomCard.tsx | 4 +-
.../JoinBeforeNavigate.tsx | 11 +-
.../message-search/SearchResultGroup.tsx | 55 ++--
src/app/features/room-nav/RoomNavItem.tsx | 19 +-
src/app/features/room/RoomTimeline.tsx | 67 ++---
src/app/features/room/RoomTombstone.tsx | 4 +-
src/app/features/room/RoomViewHeader.tsx | 240 +++++++++---------
src/app/features/room/message/Message.tsx | 32 +--
.../hooks/router/useSearchParamsViaServers.ts | 14 +
src/app/hooks/useMentionClickHandler.ts | 43 ++++
src/app/hooks/useRoomNavigate.ts | 21 +-
src/app/hooks/useSpoilerClickHandler.ts | 14 +
.../space-add-existing/SpaceAddExisting.jsx | 5 +-
src/app/pages/Router.tsx | 4 +-
src/app/pages/client/direct/DirectCreate.tsx | 33 +++
src/app/pages/client/direct/RoomProvider.tsx | 4 +-
src/app/pages/client/direct/index.ts | 1 +
src/app/pages/client/home/RoomProvider.tsx | 12 +-
src/app/pages/client/inbox/Notifications.tsx | 58 ++---
src/app/pages/client/sidebar/SpaceTabs.tsx | 19 +-
src/app/pages/client/space/RoomProvider.tsx | 12 +-
src/app/pages/client/space/Space.tsx | 20 +-
src/app/pages/client/space/SpaceProvider.tsx | 5 +-
src/app/pages/pathSearchParam.ts | 13 +
src/app/pages/paths.ts | 8 +
src/app/plugins/matrix-to.ts | 84 ++++++
src/app/plugins/react-custom-html-parser.tsx | 176 +++++++++----
src/app/plugins/via-servers.ts | 65 +++++
src/app/utils/keyboard.ts | 14 +-
src/app/utils/matrix.ts | 9 -
src/util/matrixUtil.js | 56 ----
38 files changed, 781 insertions(+), 476 deletions(-)
create mode 100644 src/app/hooks/router/useSearchParamsViaServers.ts
create mode 100644 src/app/hooks/useMentionClickHandler.ts
create mode 100644 src/app/hooks/useSpoilerClickHandler.ts
create mode 100644 src/app/pages/client/direct/DirectCreate.tsx
create mode 100644 src/app/pages/pathSearchParam.ts
create mode 100644 src/app/plugins/matrix-to.ts
create mode 100644 src/app/plugins/via-servers.ts
diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx
index 60e0331..1ce37e5 100644
--- a/src/app/components/RenderMessageContent.tsx
+++ b/src/app/components/RenderMessageContent.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { MsgType } from 'matrix-js-sdk';
import { HTMLReactParserOptions } from 'html-react-parser';
+import { Opts } from 'linkifyjs';
import {
AudioContent,
DownloadFile,
@@ -27,6 +28,7 @@ import { Image, MediaControl, Video } from './media';
import { ImageViewer } from './image-viewer';
import { PdfViewer } from './Pdf-viewer';
import { TextViewer } from './text-viewer';
+import { testMatrixTo } from '../plugins/matrix-to';
type RenderMessageContentProps = {
displayName: string;
@@ -38,6 +40,7 @@ type RenderMessageContentProps = {
urlPreview?: boolean;
highlightRegex?: RegExp;
htmlReactParserOptions: HTMLReactParserOptions;
+ linkifyOpts: Opts;
outlineAttachment?: boolean;
};
export function RenderMessageContent({
@@ -50,8 +53,21 @@ export function RenderMessageContent({
urlPreview,
highlightRegex,
htmlReactParserOptions,
+ linkifyOpts,
outlineAttachment,
}: RenderMessageContentProps) {
+ const renderUrlsPreview = (urls: string[]) => {
+ const filteredUrls = urls.filter((url) => !testMatrixTo(url));
+ if (filteredUrls.length === 0) return undefined;
+ return (
+
+ {filteredUrls.map((url) => (
+
+ ))}
+
+ );
+ };
+
const renderFile = () => (
)}
- renderUrlsPreview={
- urlPreview
- ? (urls) => (
-
- {urls.map((url) => (
-
- ))}
-
- )
- : undefined
- }
+ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
/>
);
}
@@ -123,19 +130,10 @@ export function RenderMessageContent({
{...props}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
/>
)}
- renderUrlsPreview={
- urlPreview
- ? (urls) => (
-
- {urls.map((url) => (
-
- ))}
-
- )
- : undefined
- }
+ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
/>
);
}
@@ -150,19 +148,10 @@ export function RenderMessageContent({
{...props}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
/>
)}
- renderUrlsPreview={
- urlPreview
- ? (urls) => (
-
- {urls.map((url) => (
-
- ))}
-
- )
- : undefined
- }
+ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
/>
);
}
diff --git a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
index 439d98c..049be94 100644
--- a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
+++ b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
@@ -17,6 +17,7 @@ import { mDirectAtom } from '../../../state/mDirectList';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { factoryRoomIdByActivity } from '../../../utils/sort';
import { RoomAvatar, RoomIcon } from '../../room-avatar';
+import { getViaServers } from '../../../plugins/via-servers';
type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void;
@@ -104,10 +105,14 @@ export function RoomMentionAutocomplete({
}, [query.text, search, resetSearch]);
const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => {
+ const mentionRoom = mx.getRoom(roomAliasOrId);
+ const viaServers = mentionRoom ? getViaServers(mentionRoom) : undefined;
const mentionEl = createMentionElement(
roomAliasOrId,
name.startsWith('#') ? name : `#${name}`,
- roomId === roomAliasOrId || mx.getRoom(roomId)?.getCanonicalAlias() === roomAliasOrId
+ roomId === roomAliasOrId || mx.getRoom(roomId)?.getCanonicalAlias() === roomAliasOrId,
+ undefined,
+ viaServers
);
replaceWithElement(editor, query.range, mentionEl);
moveCursor(editor, true);
diff --git a/src/app/components/editor/input.ts b/src/app/components/editor/input.ts
index 272b970..29e5bd6 100644
--- a/src/app/components/editor/input.ts
+++ b/src/app/components/editor/input.ts
@@ -18,8 +18,13 @@ import {
ParagraphElement,
UnorderedListElement,
} from './slate';
-import { parseMatrixToUrl } from '../../utils/matrix';
import { createEmoticonElement, createMentionElement } from './utils';
+import {
+ parseMatrixToRoom,
+ parseMatrixToRoomEvent,
+ parseMatrixToUser,
+ testMatrixTo,
+} from '../../plugins/matrix-to';
const markNodeToType: Record = {
b: MarkType.Bold,
@@ -68,11 +73,33 @@ const elementToInlineNode = (node: Element): MentionElement | EmoticonElement |
return createEmoticonElement(src, alt || 'Unknown Emoji');
}
if (node.name === 'a') {
- const { href } = node.attribs;
+ const href = decodeURIComponent(node.attribs.href);
if (typeof href !== 'string') return undefined;
- const [mxId] = parseMatrixToUrl(href);
- if (mxId) {
- return createMentionElement(mxId, parseNodeText(node) || mxId, false);
+ if (testMatrixTo(href)) {
+ const userMention = parseMatrixToUser(href);
+ if (userMention) {
+ return createMentionElement(userMention, parseNodeText(node) || userMention, false);
+ }
+ const roomMention = parseMatrixToRoom(href);
+ if (roomMention) {
+ return createMentionElement(
+ roomMention.roomIdOrAlias,
+ parseNodeText(node) || roomMention.roomIdOrAlias,
+ false,
+ undefined,
+ roomMention.viaServers
+ );
+ }
+ const eventMention = parseMatrixToRoomEvent(href);
+ if (eventMention) {
+ return createMentionElement(
+ eventMention.roomIdOrAlias,
+ parseNodeText(node) || eventMention.roomIdOrAlias,
+ false,
+ eventMention.eventId,
+ eventMention.viaServers
+ );
+ }
}
}
return undefined;
diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts
index 53ee6dd..864aee3 100644
--- a/src/app/components/editor/output.ts
+++ b/src/app/components/editor/output.ts
@@ -51,10 +51,19 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
case BlockType.UnorderedList:
return ``;
- case BlockType.Mention:
- return `${sanitizeText(
- node.name
- )}`;
+ case BlockType.Mention: {
+ let fragment = node.id;
+
+ if (node.eventId) {
+ fragment += `/${node.eventId}`;
+ }
+ if (node.viaServers && node.viaServers.length > 0) {
+ fragment += `?${node.viaServers.map((server) => `via=${server}`).join('&')}`;
+ }
+
+ const matrixTo = `https://matrix.to/#/${fragment}`;
+ return `${sanitizeText(node.name)}`;
+ }
case BlockType.Emoticon:
return node.key.startsWith('mxc://')
? `
- }
- radii="300"
- disabled={!unread}
- >
-
- Mark as Read
-
-
-
-
-
- }
- radii="300"
- disabled={!canInvite}
- >
-
- Invite
-
-
- }
- radii="300"
- >
-
- Copy Link
-
-
- }
- radii="300"
- >
-
- Room Settings
-
-
-
-
-
-
- {(promptLeave, setPromptLeave) => (
- <>
-
- {promptLeave && (
- setPromptLeave(false)}
- />
- )}
- >
- )}
-
-
-
- );
- }
-);
+ return (
+
+ );
+});
export function RoomViewHeader() {
const navigate = useNavigate();
@@ -195,8 +187,6 @@ export function RoomViewHeader() {
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined;
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
- const location = useLocation();
- const currentPath = joinPathComponent(location);
const handleSearchClick = () => {
const searchParams: _SearchPathSearchParams = {
@@ -336,11 +326,7 @@ export function RoomViewHeader() {
escapeDeactivates: stopPropagation,
}}
>
- setMenuAnchor(undefined)}
- />
+ setMenuAnchor(undefined)} />
}
/>
diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx
index 6db366a..d8b2b3e 100644
--- a/src/app/features/room/message/Message.tsx
+++ b/src/app/features/room/message/Message.tsx
@@ -51,7 +51,7 @@ import {
getMemberAvatarMxc,
getMemberDisplayName,
} from '../../../utils/room';
-import { getCanonicalAliasOrRoomId, getMxIdLocalPart } from '../../../utils/matrix';
+import { getCanonicalAliasOrRoomId, getMxIdLocalPart, isRoomAlias } from '../../../utils/matrix';
import { MessageLayout, MessageSpacing } from '../../../state/settings';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
@@ -63,18 +63,10 @@ import { EmojiBoard } from '../../../components/emoji-board';
import { ReactionViewer } from '../reaction-viewer';
import { MessageEditor } from './MessageEditor';
import { UserAvatar } from '../../../components/user-avatar';
-import { useSpaceOptionally } from '../../../hooks/useSpace';
-import { useDirectSelected } from '../../../hooks/router/useDirectSelected';
-import {
- getDirectRoomPath,
- getHomeRoomPath,
- getOriginBaseUrl,
- getSpaceRoomPath,
- withOriginBaseUrl,
-} from '../../../pages/pathUtils';
import { copyToClipboard } from '../../../utils/dom';
-import { useClientConfig } from '../../../hooks/useClientConfig';
import { stopPropagation } from '../../../utils/keyboard';
+import { getMatrixToRoomEvent } from '../../../plugins/matrix-to';
+import { getViaServers } from '../../../plugins/via-servers';
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
@@ -321,23 +313,13 @@ export const MessageCopyLinkItem = as<
}
>(({ room, mEvent, onClose, ...props }, ref) => {
const mx = useMatrixClient();
- const { hashRouter } = useClientConfig();
- const space = useSpaceOptionally();
- const directSelected = useDirectSelected();
const handleCopy = () => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
- let eventPath = getHomeRoomPath(roomIdOrAlias, mEvent.getId());
- if (space) {
- eventPath = getSpaceRoomPath(
- getCanonicalAliasOrRoomId(mx, space.roomId),
- roomIdOrAlias,
- mEvent.getId()
- );
- } else if (directSelected) {
- eventPath = getDirectRoomPath(roomIdOrAlias, mEvent.getId());
- }
- copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), eventPath));
+ const eventId = mEvent.getId();
+ const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
+ if (!eventId) return;
+ copyToClipboard(getMatrixToRoomEvent(roomIdOrAlias, eventId, viaServers));
onClose?.();
};
diff --git a/src/app/hooks/router/useSearchParamsViaServers.ts b/src/app/hooks/router/useSearchParamsViaServers.ts
new file mode 100644
index 0000000..0b1b2db
--- /dev/null
+++ b/src/app/hooks/router/useSearchParamsViaServers.ts
@@ -0,0 +1,14 @@
+import { useMemo } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import { getRoomSearchParams } from '../../pages/pathSearchParam';
+import { decodeSearchParamValueArray } from '../../pages/pathUtils';
+
+export const useSearchParamsViaServers = (): string[] | undefined => {
+ const [searchParams] = useSearchParams();
+ const roomSearchParams = useMemo(() => getRoomSearchParams(searchParams), [searchParams]);
+ const viaServers = roomSearchParams.viaServers
+ ? decodeSearchParamValueArray(roomSearchParams.viaServers)
+ : undefined;
+
+ return viaServers;
+};
diff --git a/src/app/hooks/useMentionClickHandler.ts b/src/app/hooks/useMentionClickHandler.ts
new file mode 100644
index 0000000..f8f4bf5
--- /dev/null
+++ b/src/app/hooks/useMentionClickHandler.ts
@@ -0,0 +1,43 @@
+import { ReactEventHandler, useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useRoomNavigate } from './useRoomNavigate';
+import { useMatrixClient } from './useMatrixClient';
+import { isRoomId, isUserId } from '../utils/matrix';
+import { openProfileViewer } from '../../client/action/navigation';
+import { getHomeRoomPath, withSearchParam } from '../pages/pathUtils';
+import { _RoomSearchParams } from '../pages/paths';
+
+export const useMentionClickHandler = (roomId: string): ReactEventHandler => {
+ const mx = useMatrixClient();
+ const { navigateRoom, navigateSpace } = useRoomNavigate();
+ const navigate = useNavigate();
+
+ const handleClick: ReactEventHandler = useCallback(
+ (evt) => {
+ evt.preventDefault();
+ const target = evt.currentTarget;
+ const mentionId = target.getAttribute('data-mention-id');
+ if (typeof mentionId !== 'string') return;
+
+ if (isUserId(mentionId)) {
+ openProfileViewer(mentionId, roomId);
+ return;
+ }
+
+ const eventId = target.getAttribute('data-mention-event-id') || undefined;
+ if (isRoomId(mentionId) && mx.getRoom(mentionId)) {
+ if (mx.getRoom(mentionId)?.isSpaceRoom()) navigateSpace(mentionId);
+ else navigateRoom(mentionId, eventId);
+ return;
+ }
+
+ const viaServers = target.getAttribute('data-mention-via') || undefined;
+ const path = getHomeRoomPath(mentionId, eventId);
+
+ navigate(viaServers ? withSearchParam<_RoomSearchParams>(path, { viaServers }) : path);
+ },
+ [mx, navigate, navigateRoom, navigateSpace, roomId]
+ );
+
+ return handleClick;
+};
diff --git a/src/app/hooks/useRoomNavigate.ts b/src/app/hooks/useRoomNavigate.ts
index 55528e7..0f9f365 100644
--- a/src/app/hooks/useRoomNavigate.ts
+++ b/src/app/hooks/useRoomNavigate.ts
@@ -1,5 +1,5 @@
import { useCallback } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { NavigateOptions, useNavigate } from 'react-router-dom';
import { useAtomValue } from 'jotai';
import { getCanonicalAliasOrRoomId } from '../utils/matrix';
import {
@@ -12,12 +12,14 @@ import { useMatrixClient } from './useMatrixClient';
import { getOrphanParents } from '../utils/room';
import { roomToParentsAtom } from '../state/room/roomToParents';
import { mDirectAtom } from '../state/mDirectList';
+import { useSelectedSpace } from './router/useSelectedSpace';
export const useRoomNavigate = () => {
const navigate = useNavigate();
const mx = useMatrixClient();
const roomToParents = useAtomValue(roomToParentsAtom);
const mDirects = useAtomValue(mDirectAtom);
+ const spaceSelectedId = useSelectedSpace();
const navigateSpace = useCallback(
(roomId: string) => {
@@ -28,24 +30,29 @@ export const useRoomNavigate = () => {
);
const navigateRoom = useCallback(
- (roomId: string, eventId?: string) => {
+ (roomId: string, eventId?: string, opts?: NavigateOptions) => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
const orphanParents = getOrphanParents(roomToParents, roomId);
if (orphanParents.length > 0) {
- const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, orphanParents[0]);
- navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId));
+ const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
+ mx,
+ spaceSelectedId && orphanParents.includes(spaceSelectedId)
+ ? spaceSelectedId
+ : orphanParents[0]
+ );
+ navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
return;
}
if (mDirects.has(roomId)) {
- navigate(getDirectRoomPath(roomIdOrAlias, eventId));
+ navigate(getDirectRoomPath(roomIdOrAlias, eventId), opts);
return;
}
- navigate(getHomeRoomPath(roomIdOrAlias, eventId));
+ navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
},
- [mx, navigate, roomToParents, mDirects]
+ [mx, navigate, spaceSelectedId, roomToParents, mDirects]
);
return {
diff --git a/src/app/hooks/useSpoilerClickHandler.ts b/src/app/hooks/useSpoilerClickHandler.ts
new file mode 100644
index 0000000..b210118
--- /dev/null
+++ b/src/app/hooks/useSpoilerClickHandler.ts
@@ -0,0 +1,14 @@
+import { ReactEventHandler, useCallback } from 'react';
+
+export const useSpoilerClickHandler = (): ReactEventHandler => {
+ const handleClick: ReactEventHandler = useCallback((evt) => {
+ const target = evt.currentTarget;
+ if (target.getAttribute('aria-pressed') === 'true') {
+ evt.stopPropagation();
+ target.setAttribute('aria-pressed', 'false');
+ target.style.cursor = 'initial';
+ }
+ }, []);
+
+ return handleClick;
+};
diff --git a/src/app/molecules/space-add-existing/SpaceAddExisting.jsx b/src/app/molecules/space-add-existing/SpaceAddExisting.jsx
index ff338f3..83b967b 100644
--- a/src/app/molecules/space-add-existing/SpaceAddExisting.jsx
+++ b/src/app/molecules/space-add-existing/SpaceAddExisting.jsx
@@ -5,7 +5,7 @@ import './SpaceAddExisting.scss';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
-import { joinRuleToIconSrc, getIdServer, genRoomVia } from '../../../util/matrixUtil';
+import { joinRuleToIconSrc, getIdServer } from '../../../util/matrixUtil';
import { Debounce } from '../../../util/common';
import Text from '../../atoms/text/Text';
@@ -27,6 +27,7 @@ import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList';
import { allRoomsAtom } from '../../state/room-list/roomList';
import { mDirectAtom } from '../../state/mDirectList';
import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { getViaServers } from '../../plugins/via-servers';
function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
const mountStore = useStore(roomId);
@@ -69,7 +70,7 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
const promises = selected.map((rId) => {
const room = mx.getRoom(rId);
- const via = genRoomVia(room);
+ const via = getViaServers(room);
if (via.length === 0) {
via.push(getIdServer(rId));
}
diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx
index 7d0f4fd..88fa993 100644
--- a/src/app/pages/Router.tsx
+++ b/src/app/pages/Router.tsx
@@ -41,7 +41,7 @@ import {
} from './pathUtils';
import { ClientBindAtoms, ClientLayout, ClientRoot } from './client';
import { Home, HomeRouteRoomProvider, HomeSearch } from './client/home';
-import { Direct, DirectRouteRoomProvider } from './client/direct';
+import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct';
import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
import { Explore, FeaturedRooms, PublicRooms } from './client/explore';
import { Notifications, Inbox, Invites } from './client/inbox';
@@ -160,7 +160,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
}
>
{mobile ? null : } />}
- create
} />
+ } />
{
+ if (userId) {
+ const room = getDMRoomFor(mx, userId);
+ const { roomId } = room ?? {};
+ if (roomId && directs.includes(roomId)) {
+ navigate(getDirectRoomPath(roomId), { replace: true });
+ } else {
+ openInviteUser(undefined, userId);
+ }
+ } else {
+ navigate(getDirectPath(), { replace: true });
+ }
+ }, [mx, navigate, directs, userId]);
+
+ return ;
+}
diff --git a/src/app/pages/client/direct/RoomProvider.tsx b/src/app/pages/client/direct/RoomProvider.tsx
index c78a8f4..ca45aa1 100644
--- a/src/app/pages/client/direct/RoomProvider.tsx
+++ b/src/app/pages/client/direct/RoomProvider.tsx
@@ -10,12 +10,12 @@ export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
const mx = useMatrixClient();
const rooms = useDirectRooms();
- const { roomIdOrAlias } = useParams();
+ const { roomIdOrAlias, eventId } = useParams();
const roomId = useSelectedRoom();
const room = mx.getRoom(roomId);
if (!room || !rooms.includes(room.roomId)) {
- return ;
+ return ;
}
return (
diff --git a/src/app/pages/client/direct/index.ts b/src/app/pages/client/direct/index.ts
index 36f44d6..d247bbc 100644
--- a/src/app/pages/client/direct/index.ts
+++ b/src/app/pages/client/direct/index.ts
@@ -1,2 +1,3 @@
export * from './Direct';
export * from './RoomProvider';
+export * from './DirectCreate';
diff --git a/src/app/pages/client/home/RoomProvider.tsx b/src/app/pages/client/home/RoomProvider.tsx
index 282cee7..aa14d15 100644
--- a/src/app/pages/client/home/RoomProvider.tsx
+++ b/src/app/pages/client/home/RoomProvider.tsx
@@ -5,17 +5,25 @@ import { RoomProvider } from '../../../hooks/useRoom';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
import { useHomeRooms } from './useHomeRooms';
+import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
export function HomeRouteRoomProvider({ children }: { children: ReactNode }) {
const mx = useMatrixClient();
const rooms = useHomeRooms();
- const { roomIdOrAlias } = useParams();
+ const { roomIdOrAlias, eventId } = useParams();
+ const viaServers = useSearchParamsViaServers();
const roomId = useSelectedRoom();
const room = mx.getRoom(roomId);
if (!room || !rooms.includes(room.roomId)) {
- return ;
+ return (
+
+ );
}
return (
diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx
index a01ecb8..3425b51 100644
--- a/src/app/pages/client/inbox/Notifications.tsx
+++ b/src/app/pages/client/inbox/Notifications.tsx
@@ -24,9 +24,10 @@ import {
} from 'matrix-js-sdk';
import { useVirtualizer } from '@tanstack/react-virtual';
import { HTMLReactParserOptions } from 'html-react-parser';
+import { Opts as LinkifyOpts } from 'linkifyjs';
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
-import { getMxIdLocalPart, isRoomId, isUserId } from '../../../utils/matrix';
+import { getMxIdLocalPart } from '../../../utils/matrix';
import { InboxNotificationsPathSearchParams } from '../../paths';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { SequenceCard } from '../../../components/sequence-card';
@@ -52,8 +53,13 @@ import {
Username,
} from '../../../components/message';
import colorMXID from '../../../../util/colorMXID';
-import { getReactCustomHtmlParser } from '../../../plugins/react-custom-html-parser';
-import { openJoinAlias, openProfileViewer } from '../../../../client/action/navigation';
+import {
+ factoryRenderLinkifyWithMention,
+ getReactCustomHtmlParser,
+ LINKIFY_OPTS,
+ makeMentionCustomProps,
+ renderMatrixMention,
+} from '../../../plugins/react-custom-html-parser';
import { RenderMessageContent } from '../../../components/RenderMessageContent';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
@@ -70,6 +76,8 @@ import { ContainerColor } from '../../../styles/ContainerColor.css';
import { VirtualTile } from '../../../components/virtualizer';
import { UserAvatar } from '../../../components/user-avatar';
import { EncryptedContent } from '../../../features/room/message';
+import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
+import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
type RoomNotificationsGroup = {
roomId: string;
@@ -181,36 +189,26 @@ function RoomNotificationsGroupComp({
}: RoomNotificationsGroupProps) {
const mx = useMatrixClient();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
- const { navigateRoom, navigateSpace } = useRoomNavigate();
+ const mentionClickHandler = useMentionClickHandler(room.roomId);
+ const spoilerClickHandler = useSpoilerClickHandler();
+ const linkifyOpts = useMemo(
+ () => ({
+ ...LINKIFY_OPTS,
+ render: factoryRenderLinkifyWithMention((href) =>
+ renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
+ ),
+ }),
+ [mx, room, mentionClickHandler]
+ );
const htmlReactParserOptions = useMemo(
() =>
- getReactCustomHtmlParser(mx, room, {
- handleSpoilerClick: (evt) => {
- const target = evt.currentTarget;
- if (target.getAttribute('aria-pressed') === 'true') {
- evt.stopPropagation();
- target.setAttribute('aria-pressed', 'false');
- target.style.cursor = 'initial';
- }
- },
- handleMentionClick: (evt) => {
- const target = evt.currentTarget;
- const mentionId = target.getAttribute('data-mention-id');
- if (typeof mentionId !== 'string') return;
- if (isUserId(mentionId)) {
- openProfileViewer(mentionId, room.roomId);
- return;
- }
- if (isRoomId(mentionId) && mx.getRoom(mentionId)) {
- if (mx.getRoom(mentionId)?.isSpaceRoom()) navigateSpace(mentionId);
- else navigateRoom(mentionId);
- return;
- }
- openJoinAlias(mentionId);
- },
+ getReactCustomHtmlParser(mx, room.roomId, {
+ linkifyOpts,
+ handleSpoilerClick: spoilerClickHandler,
+ handleMentionClick: mentionClickHandler,
}),
- [mx, room, navigateRoom, navigateSpace]
+ [mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler]
);
const renderMatrixEvent = useMatrixEventRenderer<[IRoomEvent, string, GetContentCallback]>(
@@ -229,6 +227,7 @@ function RoomNotificationsGroupComp({
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
outlineAttachment
/>
);
@@ -287,6 +286,7 @@ function RoomNotificationsGroupComp({
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
/>
);
}
diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx
index 7b3e61e..f14976f 100644
--- a/src/app/pages/client/sidebar/SpaceTabs.tsx
+++ b/src/app/pages/client/sidebar/SpaceTabs.tsx
@@ -47,13 +47,7 @@ import {
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { allRoomsAtom } from '../../../state/room-list/roomList';
-import {
- getOriginBaseUrl,
- getSpaceLobbyPath,
- getSpacePath,
- joinPathComponent,
- withOriginBaseUrl,
-} from '../../pathUtils';
+import { getSpaceLobbyPath, getSpacePath, joinPathComponent } from '../../pathUtils';
import {
SidebarAvatar,
SidebarItem,
@@ -67,7 +61,7 @@ import {
import { RoomUnreadProvider, RoomsUnreadProvider } from '../../../components/RoomUnreadProvider';
import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace';
import { UnreadBadge } from '../../../components/unread-badge';
-import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
+import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix';
import { RoomAvatar } from '../../../components/room-avatar';
import { nameInitials, randomStr } from '../../../utils/common';
import {
@@ -83,7 +77,6 @@ import { AccountDataEvent } from '../../../../types/matrix/accountData';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
import { useOpenedSidebarFolderAtom } from '../../../state/hooks/openedSidebarFolder';
-import { useClientConfig } from '../../../hooks/useClientConfig';
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
import { useRoomsUnread } from '../../../state/hooks/unread';
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
@@ -91,6 +84,8 @@ import { markAsRead } from '../../../../client/action/notifications';
import { copyToClipboard } from '../../../utils/dom';
import { openInviteUser, openSpaceSettings } from '../../../../client/action/navigation';
import { stopPropagation } from '../../../utils/keyboard';
+import { getMatrixToRoom } from '../../../plugins/matrix-to';
+import { getViaServers } from '../../../plugins/via-servers';
type SpaceMenuProps = {
room: Room;
@@ -100,7 +95,6 @@ type SpaceMenuProps = {
const SpaceMenu = forwardRef(
({ room, requestClose, onUnpin }, ref) => {
const mx = useMatrixClient();
- const { hashRouter } = useClientConfig();
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
@@ -124,8 +118,9 @@ const SpaceMenu = forwardRef(
};
const handleCopyLink = () => {
- const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, room.roomId));
- copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), spacePath));
+ const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
+ const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
+ copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
requestClose();
};
diff --git a/src/app/pages/client/space/RoomProvider.tsx b/src/app/pages/client/space/RoomProvider.tsx
index 1105e22..0f13f93 100644
--- a/src/app/pages/client/space/RoomProvider.tsx
+++ b/src/app/pages/client/space/RoomProvider.tsx
@@ -9,6 +9,7 @@ import { useSpace } from '../../../hooks/useSpace';
import { getAllParents } from '../../../utils/room';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { allRoomsAtom } from '../../../state/room-list/roomList';
+import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
const mx = useMatrixClient();
@@ -16,7 +17,8 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
const roomToParents = useAtomValue(roomToParentsAtom);
const allRooms = useAtomValue(allRoomsAtom);
- const { roomIdOrAlias } = useParams();
+ const { roomIdOrAlias, eventId } = useParams();
+ const viaServers = useSearchParamsViaServers();
const roomId = useSelectedRoom();
const room = mx.getRoom(roomId);
@@ -26,7 +28,13 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
!allRooms.includes(room.roomId) ||
!getAllParents(roomToParents, room.roomId).has(space.roomId)
) {
- return ;
+ return (
+
+ );
}
return (
diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx
index e280c60..d3dc0be 100644
--- a/src/app/pages/client/space/Space.tsx
+++ b/src/app/pages/client/space/Space.tsx
@@ -34,15 +34,8 @@ import {
NavItemContent,
NavLink,
} from '../../../components/nav';
-import {
- getOriginBaseUrl,
- getSpaceLobbyPath,
- getSpacePath,
- getSpaceRoomPath,
- getSpaceSearchPath,
- withOriginBaseUrl,
-} from '../../pathUtils';
-import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
+import { getSpaceLobbyPath, getSpaceRoomPath, getSpaceSearchPath } from '../../pathUtils';
+import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix';
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
import {
useSpaceLobbySelected,
@@ -69,11 +62,12 @@ import { useRoomsUnread } from '../../../state/hooks/unread';
import { UseStateProvider } from '../../../components/UseStateProvider';
import { LeaveSpacePrompt } from '../../../components/leave-space-prompt';
import { copyToClipboard } from '../../../utils/dom';
-import { useClientConfig } from '../../../hooks/useClientConfig';
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
import { useStateEvent } from '../../../hooks/useStateEvent';
import { StateEvent } from '../../../../types/matrix/room';
import { stopPropagation } from '../../../utils/keyboard';
+import { getMatrixToRoom } from '../../../plugins/matrix-to';
+import { getViaServers } from '../../../plugins/via-servers';
type SpaceMenuProps = {
room: Room;
@@ -81,7 +75,6 @@ type SpaceMenuProps = {
};
const SpaceMenu = forwardRef(({ room, requestClose }, ref) => {
const mx = useMatrixClient();
- const { hashRouter } = useClientConfig();
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
@@ -100,8 +93,9 @@ const SpaceMenu = forwardRef(({ room, requestClo
};
const handleCopyLink = () => {
- const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, room.roomId));
- copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), spacePath));
+ const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
+ const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
+ copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
requestClose();
};
diff --git a/src/app/pages/client/space/SpaceProvider.tsx b/src/app/pages/client/space/SpaceProvider.tsx
index 530fc3c..2e0f79a 100644
--- a/src/app/pages/client/space/SpaceProvider.tsx
+++ b/src/app/pages/client/space/SpaceProvider.tsx
@@ -6,6 +6,7 @@ import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace';
import { SpaceProvider } from '../../../hooks/useSpace';
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
+import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
type RouteSpaceProviderProps = {
children: ReactNode;
@@ -13,13 +14,15 @@ type RouteSpaceProviderProps = {
export function RouteSpaceProvider({ children }: RouteSpaceProviderProps) {
const mx = useMatrixClient();
const joinedSpaces = useSpaces(mx, allRoomsAtom);
+
const { spaceIdOrAlias } = useParams();
+ const viaServers = useSearchParamsViaServers();
const selectedSpaceId = useSelectedSpace();
const space = mx.getRoom(selectedSpaceId);
if (!space || !joinedSpaces.includes(space.roomId)) {
- return ;
+ return ;
}
return (
diff --git a/src/app/pages/pathSearchParam.ts b/src/app/pages/pathSearchParam.ts
new file mode 100644
index 0000000..8e4c93e
--- /dev/null
+++ b/src/app/pages/pathSearchParam.ts
@@ -0,0 +1,13 @@
+import { _RoomSearchParams, DirectCreateSearchParams } from './paths';
+
+type SearchParamsGetter = (searchParams: URLSearchParams) => T;
+
+export const getRoomSearchParams: SearchParamsGetter<_RoomSearchParams> = (searchParams) => ({
+ viaServers: searchParams.get('viaServers') ?? undefined,
+});
+
+export const getDirectCreateSearchParams: SearchParamsGetter = (
+ searchParams
+) => ({
+ userId: searchParams.get('userId') ?? undefined,
+});
diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts
index fd3266e..5775038 100644
--- a/src/app/pages/paths.ts
+++ b/src/app/pages/paths.ts
@@ -35,6 +35,11 @@ export type _SearchPathSearchParams = {
senders?: string;
};
export const _SEARCH_PATH = 'search/';
+
+export type _RoomSearchParams = {
+ /* comma separated string of servers */
+ viaServers?: string;
+};
export const _ROOM_PATH = ':roomIdOrAlias/:eventId?/';
export const HOME_PATH = '/home/';
@@ -44,6 +49,9 @@ export const HOME_SEARCH_PATH = `/home/${_SEARCH_PATH}`;
export const HOME_ROOM_PATH = `/home/${_ROOM_PATH}`;
export const DIRECT_PATH = '/direct/';
+export type DirectCreateSearchParams = {
+ userId?: string;
+};
export const DIRECT_CREATE_PATH = `/direct/${_CREATE_PATH}`;
export const DIRECT_ROOM_PATH = `/direct/${_ROOM_PATH}`;
diff --git a/src/app/plugins/matrix-to.ts b/src/app/plugins/matrix-to.ts
new file mode 100644
index 0000000..c9df0a8
--- /dev/null
+++ b/src/app/plugins/matrix-to.ts
@@ -0,0 +1,84 @@
+const MATRIX_TO_BASE = 'https://matrix.to';
+
+export const getMatrixToUser = (userId: string): string => `${MATRIX_TO_BASE}/#/${userId}`;
+
+const withViaServers = (fragment: string, viaServers: string[]): string =>
+ `${fragment}?${viaServers.map((server) => `via=${server}`).join('&')}`;
+
+export const getMatrixToRoom = (roomIdOrAlias: string, viaServers?: string[]): string => {
+ let fragment = roomIdOrAlias;
+
+ if (Array.isArray(viaServers) && viaServers.length > 0) {
+ fragment = withViaServers(fragment, viaServers);
+ }
+
+ return `${MATRIX_TO_BASE}/#/${fragment}`;
+};
+
+export const getMatrixToRoomEvent = (
+ roomIdOrAlias: string,
+ eventId: string,
+ viaServers?: string[]
+): string => {
+ let fragment = `${roomIdOrAlias}/${eventId}`;
+
+ if (Array.isArray(viaServers) && viaServers.length > 0) {
+ fragment = withViaServers(fragment, viaServers);
+ }
+
+ return `${MATRIX_TO_BASE}/#/${fragment}`;
+};
+
+export type MatrixToRoom = {
+ roomIdOrAlias: string;
+ viaServers?: string[];
+};
+
+export type MatrixToRoomEvent = MatrixToRoom & {
+ eventId: string;
+};
+
+const MATRIX_TO = /^https?:\/\/matrix\.to\S*$/;
+export const testMatrixTo = (href: string): boolean => MATRIX_TO.test(href);
+
+const MATRIX_TO_USER = /^https?:\/\/matrix\.to\/#\/(@[^:\s]+:[^?/\s]+)\/?$/;
+const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/?(\?[\S]*)?$/;
+const MATRIX_TO_ROOM_EVENT =
+ /^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/;
+
+export const parseMatrixToUser = (href: string): string | undefined => {
+ const match = href.match(MATRIX_TO_USER);
+ if (!match) return undefined;
+ const userId = match[1];
+ return userId;
+};
+
+export const parseMatrixToRoom = (href: string): MatrixToRoom | undefined => {
+ const match = href.match(MATRIX_TO_ROOM);
+ if (!match) return undefined;
+
+ const roomIdOrAlias = match[1];
+ const viaSearchStr = match[2];
+ const viaServers = new URLSearchParams(viaSearchStr).getAll('via');
+
+ return {
+ roomIdOrAlias,
+ viaServers: viaServers.length === 0 ? undefined : viaServers,
+ };
+};
+
+export const parseMatrixToRoomEvent = (href: string): MatrixToRoomEvent | undefined => {
+ const match = href.match(MATRIX_TO_ROOM_EVENT);
+ if (!match) return undefined;
+
+ const roomIdOrAlias = match[1];
+ const eventId = match[2];
+ const viaSearchStr = match[3];
+ const viaServers = new URLSearchParams(viaSearchStr).getAll('via');
+
+ return {
+ roomIdOrAlias,
+ eventId,
+ viaServers: viaServers.length === 0 ? undefined : viaServers,
+ };
+};
diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx
index a808668..1670437 100644
--- a/src/app/plugins/react-custom-html-parser.tsx
+++ b/src/app/plugins/react-custom-html-parser.tsx
@@ -1,5 +1,5 @@
/* eslint-disable jsx-a11y/alt-text */
-import React, { ReactEventHandler, Suspense, lazy } from 'react';
+import React, { ComponentPropsWithoutRef, ReactEventHandler, Suspense, lazy } from 'react';
import {
Element,
Text as DOMText,
@@ -7,18 +7,25 @@ import {
attributesToProps,
domToReact,
} from 'html-react-parser';
-import { MatrixClient, Room } from 'matrix-js-sdk';
+import { MatrixClient } from 'matrix-js-sdk';
import classNames from 'classnames';
import { Scroll, Text } from 'folds';
-import { Opts as LinkifyOpts } from 'linkifyjs';
+import { IntermediateRepresentation, Opts as LinkifyOpts, OptFn } from 'linkifyjs';
import Linkify from 'linkify-react';
import { ErrorBoundary } from 'react-error-boundary';
import * as css from '../styles/CustomHtml.css';
-import { getMxIdLocalPart, getCanonicalAliasRoomId } from '../utils/matrix';
+import { getMxIdLocalPart, getCanonicalAliasRoomId, isRoomAlias } from '../utils/matrix';
import { getMemberDisplayName } from '../utils/room';
import { EMOJI_PATTERN, URL_NEG_LB } from '../utils/regex';
import { getHexcodeForEmoji, getShortcodeFor } from './emoji';
import { findAndReplace } from '../utils/findAndReplace';
+import {
+ parseMatrixToRoom,
+ parseMatrixToRoomEvent,
+ parseMatrixToUser,
+ testMatrixTo,
+} from './matrix-to';
+import { onEnterOrSpace } from '../utils/keyboard';
const ReactPrism = lazy(() => import('./react-prism/ReactPrism'));
@@ -35,6 +42,108 @@ export const LINKIFY_OPTS: LinkifyOpts = {
ignoreTags: ['span'],
};
+export const makeMentionCustomProps = (
+ handleMentionClick?: ReactEventHandler
+): ComponentPropsWithoutRef<'a'> => ({
+ style: { cursor: 'pointer' },
+ target: '_blank',
+ rel: 'noreferrer noopener',
+ role: 'link',
+ tabIndex: handleMentionClick ? 0 : -1,
+ onKeyDown: handleMentionClick ? onEnterOrSpace(handleMentionClick) : undefined,
+ onClick: handleMentionClick,
+});
+
+export const renderMatrixMention = (
+ mx: MatrixClient,
+ currentRoomId: string | undefined,
+ href: string,
+ customProps: ComponentPropsWithoutRef<'a'>
+) => {
+ const userId = parseMatrixToUser(href);
+ if (userId) {
+ const currentRoom = mx.getRoom(currentRoomId);
+
+ return (
+
+ {`@${
+ (currentRoom && getMemberDisplayName(currentRoom, userId)) ?? getMxIdLocalPart(userId)
+ }`}
+
+ );
+ }
+
+ const matrixToRoom = parseMatrixToRoom(href);
+ if (matrixToRoom) {
+ const { roomIdOrAlias, viaServers } = matrixToRoom;
+ const mentionRoom = mx.getRoom(
+ isRoomAlias(roomIdOrAlias) ? getCanonicalAliasRoomId(mx, roomIdOrAlias) : roomIdOrAlias
+ );
+
+ return (
+
+ {mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias}
+
+ );
+ }
+
+ const matrixToRoomEvent = parseMatrixToRoomEvent(href);
+ if (matrixToRoomEvent) {
+ const { roomIdOrAlias, eventId, viaServers } = matrixToRoomEvent;
+ const mentionRoom = mx.getRoom(
+ isRoomAlias(roomIdOrAlias) ? getCanonicalAliasRoomId(mx, roomIdOrAlias) : roomIdOrAlias
+ );
+
+ return (
+
+ Message: {mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias}
+
+ );
+ }
+
+ return undefined;
+};
+
+export const factoryRenderLinkifyWithMention = (
+ mentionRender: (href: string) => JSX.Element | undefined
+): OptFn<(ir: IntermediateRepresentation) => any> => {
+ const render: OptFn<(ir: IntermediateRepresentation) => any> = ({
+ tagName,
+ attributes,
+ content,
+ }) => {
+ if (tagName === 'a' && testMatrixTo(decodeURIComponent(attributes.href))) {
+ const mention = mentionRender(decodeURIComponent(attributes.href));
+ if (mention) return mention;
+ }
+
+ return {content};
+ };
+ return render;
+};
+
export const scaleSystemEmoji = (text: string): (string | JSX.Element)[] =>
findAndReplace(
text,
@@ -76,8 +185,9 @@ export const highlightText = (
export const getReactCustomHtmlParser = (
mx: MatrixClient,
- room: Room,
+ roomId: string | undefined,
params: {
+ linkifyOpts: LinkifyOpts;
highlightRegex?: RegExp;
handleSpoilerClick?: ReactEventHandler;
handleMentionClick?: ReactEventHandler;
@@ -215,54 +325,14 @@ export const getReactCustomHtmlParser = (
}
}
- if (name === 'a') {
- const mention = decodeURIComponent(props.href).match(
- /^https?:\/\/matrix.to\/#\/((@|#|!).+:[^?/]+)/
+ if (name === 'a' && testMatrixTo(decodeURIComponent(props.href))) {
+ const mention = renderMatrixMention(
+ mx,
+ roomId,
+ decodeURIComponent(props.href),
+ makeMentionCustomProps(params.handleMentionClick)
);
- if (mention) {
- // convert mention link to pill
- const mentionId = mention[1];
- const mentionPrefix = mention[2];
- if (mentionPrefix === '#' || mentionPrefix === '!') {
- const mentionRoom = mx.getRoom(
- mentionPrefix === '#' ? getCanonicalAliasRoomId(mx, mentionId) : mentionId
- );
-
- return (
-
- {domToReact(children, opts)}
-
- );
- }
- if (mentionPrefix === '@')
- return (
-
- {`@${getMemberDisplayName(room, mentionId) ?? getMxIdLocalPart(mentionId)}`}
-
- );
- }
+ if (mention) return mention;
}
if (name === 'span' && 'data-mx-spoiler' in props) {
@@ -316,7 +386,7 @@ export const getReactCustomHtmlParser = (
}
if (linkify) {
- return {jsx};
+ return {jsx};
}
return jsx;
}
diff --git a/src/app/plugins/via-servers.ts b/src/app/plugins/via-servers.ts
new file mode 100644
index 0000000..7547099
--- /dev/null
+++ b/src/app/plugins/via-servers.ts
@@ -0,0 +1,65 @@
+import { Room } from 'matrix-js-sdk';
+import { IPowerLevels } from '../hooks/usePowerLevels';
+import { getMxIdServer } from '../utils/matrix';
+import { StateEvent } from '../../types/matrix/room';
+import { getStateEvent } from '../utils/room';
+
+export const getViaServers = (room: Room): string[] => {
+ const getHighestPowerUserId = (): string | undefined => {
+ const powerLevels = getStateEvent(room, StateEvent.RoomPowerLevels)?.getContent();
+
+ if (!powerLevels) return undefined;
+ const userIdToPower = powerLevels.users;
+ if (!userIdToPower) return undefined;
+ let powerUserId: string | undefined;
+
+ Object.keys(userIdToPower).forEach((userId) => {
+ if (userIdToPower[userId] <= (powerLevels.users_default ?? 0)) return;
+
+ if (!powerUserId) {
+ powerUserId = userId;
+ return;
+ }
+ if (userIdToPower[userId] > userIdToPower[powerUserId]) {
+ powerUserId = userId;
+ }
+ });
+ return powerUserId;
+ };
+
+ const getServerToPopulation = (): Record => {
+ const members = room.getMembers();
+ const serverToPop: Record = {};
+
+ members?.forEach((member) => {
+ const { userId } = member;
+ const server = getMxIdServer(userId);
+ if (!server) return;
+ const serverPop = serverToPop[server];
+ if (serverPop === undefined) {
+ serverToPop[server] = 1;
+ return;
+ }
+ serverToPop[server] = serverPop + 1;
+ });
+
+ return serverToPop;
+ };
+
+ const via: string[] = [];
+ const userId = getHighestPowerUserId();
+ if (userId) {
+ const server = getMxIdServer(userId);
+ if (server) via.push(server);
+ }
+ const serverToPop = getServerToPopulation();
+ const sortedServers = Object.keys(serverToPop).sort(
+ (svrA, svrB) => serverToPop[svrB] - serverToPop[svrA]
+ );
+ const mostPop3 = sortedServers.slice(0, 3);
+ if (via.length === 0) return mostPop3;
+ if (mostPop3.includes(via[0])) {
+ mostPop3.splice(mostPop3.indexOf(via[0]), 1);
+ }
+ return via.concat(mostPop3.slice(0, 2));
+};
diff --git a/src/app/utils/keyboard.ts b/src/app/utils/keyboard.ts
index da3fe8c..46a951f 100644
--- a/src/app/utils/keyboard.ts
+++ b/src/app/utils/keyboard.ts
@@ -24,12 +24,14 @@ export const preventScrollWithArrowKey: KeyboardEventHandler = (evt) => {
}
};
-export const onEnterOrSpace = (callback: () => void) => (evt: KeyboardEventLike) => {
- if (isKeyHotkey('enter', evt) || isKeyHotkey('space', evt)) {
- evt.preventDefault();
- callback();
- }
-};
+export const onEnterOrSpace =
+ (callback: (evt: T) => void) =>
+ (evt: KeyboardEventLike) => {
+ if (isKeyHotkey('enter', evt) || isKeyHotkey('space', evt)) {
+ evt.preventDefault();
+ callback(evt as T);
+ }
+ };
export const stopPropagation = (evt: KeyboardEvent): boolean => {
evt.stopPropagation();
diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts
index 278bb46..f837ed4 100644
--- a/src/app/utils/matrix.ts
+++ b/src/app/utils/matrix.ts
@@ -32,15 +32,6 @@ export const isRoomId = (id: string): boolean => validMxId(id) && id.startsWith(
export const isRoomAlias = (id: string): boolean => validMxId(id) && id.startsWith('#');
-export const parseMatrixToUrl = (url: string): [string | undefined, string | undefined] => {
- const href = decodeURIComponent(url);
-
- const match = href.match(/^https?:\/\/matrix.to\/#\/([@!$+#]\S+:[^\\?|^\s|^\\/]+)(\?(via=\S+))?/);
- if (!match) return [undefined, undefined];
- const [, g1AsMxId, , g3AsVia] = match;
- return [g1AsMxId, g3AsVia];
-};
-
export const getCanonicalAliasRoomId = (mx: MatrixClient, alias: string): string | undefined =>
mx.getRooms()?.find((room) => room.getCanonicalAlias() === alias)?.roomId;
diff --git a/src/util/matrixUtil.js b/src/util/matrixUtil.js
index e4fd40c..9f1d942 100644
--- a/src/util/matrixUtil.js
+++ b/src/util/matrixUtil.js
@@ -95,67 +95,11 @@ export function joinRuleToIconSrc(joinRule, isSpace) {
}[joinRule]?.() || null);
}
-// NOTE: it gives userId with minimum power level 50;
-function getHighestPowerUserId(room) {
- const userIdToPower = room.currentState.getStateEvents('m.room.power_levels', '')?.getContent().users;
- let powerUserId = null;
- if (!userIdToPower) return powerUserId;
-
- Object.keys(userIdToPower).forEach((userId) => {
- if (userIdToPower[userId] < 50) return;
- if (powerUserId === null) {
- powerUserId = userId;
- return;
- }
- if (userIdToPower[userId] > userIdToPower[powerUserId]) {
- powerUserId = userId;
- }
- });
- return powerUserId;
-}
-
export function getIdServer(userId) {
const idParts = userId.split(':');
return idParts[1];
}
-export function getServerToPopulation(room) {
- const members = room.getMembers();
- const serverToPop = {};
-
- members?.forEach((member) => {
- const { userId } = member;
- const server = getIdServer(userId);
- const serverPop = serverToPop[server];
- if (serverPop === undefined) {
- serverToPop[server] = 1;
- return;
- }
- serverToPop[server] = serverPop + 1;
- });
-
- return serverToPop;
-}
-
-export function genRoomVia(room) {
- const via = [];
- const userId = getHighestPowerUserId(room);
- if (userId) {
- const server = getIdServer(userId);
- if (server) via.push(server);
- }
- const serverToPop = getServerToPopulation(room);
- const sortedServers = Object.keys(serverToPop).sort(
- (svrA, svrB) => serverToPop[svrB] - serverToPop[svrA],
- );
- const mostPop3 = sortedServers.slice(0, 3);
- if (via.length === 0) return mostPop3;
- if (mostPop3.includes(via[0])) {
- mostPop3.splice(mostPop3.indexOf(via[0]), 1);
- }
- return via.concat(mostPop3.slice(0, 2));
-}
-
export function isCrossVerified(mx, deviceId) {
try {
const crossSignInfo = mx.getStoredCrossSigningForUser(mx.getUserId());