import React from 'react';
import { MsgType } from 'matrix-js-sdk';
import { HTMLReactParserOptions } from 'html-react-parser';
+import { Opts } from 'linkifyjs';
import {
AudioContent,
DownloadFile,
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;
urlPreview?: boolean;
highlightRegex?: RegExp;
htmlReactParserOptions: HTMLReactParserOptions;
+ linkifyOpts: Opts;
outlineAttachment?: boolean;
};
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 (
+ <UrlPreviewHolder>
+ {filteredUrls.map((url) => (
+ <UrlPreviewCard key={url} url={url} ts={ts} />
+ ))}
+ </UrlPreviewHolder>
+ );
+ };
+
const renderFile = () => (
<MFile
content={getContent()}
{...props}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
/>
)}
- renderUrlsPreview={
- urlPreview
- ? (urls) => (
- <UrlPreviewHolder>
- {urls.map((url) => (
- <UrlPreviewCard key={url} url={url} ts={ts} />
- ))}
- </UrlPreviewHolder>
- )
- : undefined
- }
+ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
/>
);
}
{...props}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
/>
)}
- renderUrlsPreview={
- urlPreview
- ? (urls) => (
- <UrlPreviewHolder>
- {urls.map((url) => (
- <UrlPreviewCard key={url} url={url} ts={ts} />
- ))}
- </UrlPreviewHolder>
- )
- : undefined
- }
+ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
/>
);
}
{...props}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
/>
)}
- renderUrlsPreview={
- urlPreview
- ? (urls) => (
- <UrlPreviewHolder>
- {urls.map((url) => (
- <UrlPreviewCard key={url} url={url} ts={ts} />
- ))}
- </UrlPreviewHolder>
- )
- : undefined
- }
+ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
/>
);
}
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;
}, [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);
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<string, MarkType> = {
b: MarkType.Bold,
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;
case BlockType.UnorderedList:
return `<ul>${children}</ul>`;
- case BlockType.Mention:
- return `<a href="https://matrix.to/#/${encodeURIComponent(node.id)}">${sanitizeText(
- node.name
- )}</a>`;
+ 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 `<a href="${encodeURIComponent(matrixTo)}">${sanitizeText(node.name)}</a>`;
+ }
case BlockType.Emoticon:
return node.key.startsWith('mxc://')
? `<img data-mx-emoticon src="${node.key}" alt="${sanitizeText(
export type MentionElement = {
type: BlockType.Mention;
id: string;
+ eventId?: string;
+ viaServers?: string[];
highlight: boolean;
name: string;
children: Text[];
export const createMentionElement = (
id: string,
name: string,
- highlight: boolean
+ highlight: boolean,
+ eventId?: string,
+ viaServers?: string[]
): MentionElement => ({
type: BlockType.Mention,
id,
+ eventId,
+ viaServers,
highlight,
name,
children: [{ text: '' }],
import React from 'react';
import parse, { HTMLReactParserOptions } from 'html-react-parser';
import Linkify from 'linkify-react';
+import { Opts } from 'linkifyjs';
import { MessageEmptyContent } from './content';
import { sanitizeCustomHtml } from '../../utils/sanitize';
-import {
- LINKIFY_OPTS,
- highlightText,
- scaleSystemEmoji,
-} from '../../plugins/react-custom-html-parser';
+import { highlightText, scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
type RenderBodyProps = {
body: string;
highlightRegex?: RegExp;
htmlReactParserOptions: HTMLReactParserOptions;
+ linkifyOpts: Opts;
};
export function RenderBody({
body,
customBody,
highlightRegex,
htmlReactParserOptions,
+ linkifyOpts,
}: RenderBodyProps) {
if (body === '') <MessageEmptyContent />;
if (customBody) {
return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions);
}
return (
- <Linkify options={LINKIFY_OPTS}>
+ <Linkify options={linkifyOpts}>
{highlightRegex
? highlightText(highlightRegex, scaleSystemEmoji(body))
: scaleSystemEmoji(body)}
topic?: string;
memberCount?: number;
roomType?: string;
+ viaServers?: string[];
onView?: (roomId: string) => void;
renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode;
};
topic,
memberCount,
roomType,
+ viaServers,
onView,
renderTopicViewer,
...props
);
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
- useCallback(() => mx.joinRoom(roomIdOrAlias), [mx, roomIdOrAlias])
+ useCallback(() => mx.joinRoom(roomIdOrAlias, { viaServers }), [mx, roomIdOrAlias, viaServers])
);
const joining =
joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success;
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../state/room-list/roomList';
-type JoinBeforeNavigateProps = { roomIdOrAlias: string };
-export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) {
+type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] };
+export function JoinBeforeNavigate({
+ roomIdOrAlias,
+ eventId,
+ viaServers,
+}: JoinBeforeNavigateProps) {
const mx = useMatrixClient();
const allRooms = useAtomValue(allRoomsAtom);
const { navigateRoom, navigateSpace } = useRoomNavigate();
navigateSpace(roomId);
return;
}
- navigateRoom(roomId);
+ navigateRoom(roomId, eventId);
};
return (
topic={summary?.topic}
memberCount={summary?.num_joined_members}
roomType={summary?.room_type}
+ viaServers={viaServers}
renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer name={name} topic={topic} requestClose={requestClose} />
)}
import { IEventWithRoomId, JoinRule, RelationType, Room } from 'matrix-js-sdk';
import { HTMLReactParserOptions } from 'html-react-parser';
import { Avatar, Box, Chip, Header, Icon, Icons, Text, config } from 'folds';
+import { Opts as LinkifyOpts } from 'linkifyjs';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import {
+ factoryRenderLinkifyWithMention,
getReactCustomHtmlParser,
+ LINKIFY_OPTS,
makeHighlightRegex,
+ makeMentionCustomProps,
+ renderMatrixMention,
} from '../../plugins/react-custom-html-parser';
-import { getMxIdLocalPart, isRoomId, isUserId } from '../../utils/matrix';
-import { openJoinAlias, openProfileViewer } from '../../../client/action/navigation';
+import { getMxIdLocalPart } from '../../utils/matrix';
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
import {
import colorMXID from '../../../util/colorMXID';
import { ResultItem } from './useMessageSearch';
import { SequenceCard } from '../../components/sequence-card';
-import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { UserAvatar } from '../../components/user-avatar';
+import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
+import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
type SearchResultGroupProps = {
room: Room;
onOpen,
}: SearchResultGroupProps) {
const mx = useMatrixClient();
- const { navigateRoom, navigateSpace } = useRoomNavigate();
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]);
+ const mentionClickHandler = useMentionClickHandler(room.roomId);
+ const spoilerClickHandler = useSpoilerClickHandler();
+
+ const linkifyOpts = useMemo<LinkifyOpts>(
+ () => ({
+ ...LINKIFY_OPTS,
+ render: factoryRenderLinkifyWithMention((href) =>
+ renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
+ ),
+ }),
+ [mx, room, mentionClickHandler]
+ );
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
() =>
- getReactCustomHtmlParser(mx, room, {
+ getReactCustomHtmlParser(mx, room.roomId, {
+ linkifyOpts,
highlightRegex,
- 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);
- },
+ handleSpoilerClick: spoilerClickHandler,
+ handleMentionClick: mentionClickHandler,
}),
- [mx, room, highlightRegex, navigateRoom, navigateSpace]
+ [mx, room, linkifyOpts, highlightRegex, mentionClickHandler, spoilerClickHandler]
);
const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>(
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
highlightRegex={highlightRegex}
outlineAttachment
/>
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
import { copyToClipboard } from '../../utils/dom';
-import { getOriginBaseUrl, withOriginBaseUrl } from '../../pages/pathUtils';
import { markAsRead } from '../../../client/action/notifications';
import { openInviteUser, toggleRoomSettings } from '../../../client/action/navigation';
import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
-import { useClientConfig } from '../../hooks/useClientConfig';
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { TypingIndicator } from '../../components/typing-indicator';
import { stopPropagation } from '../../utils/keyboard';
+import { getMatrixToRoom } from '../../plugins/matrix-to';
+import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
+import { getViaServers } from '../../plugins/via-servers';
type RoomNavItemMenuProps = {
room: Room;
- linkPath: string;
requestClose: () => void;
};
const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
- ({ room, linkPath, requestClose }, ref) => {
+ ({ room, requestClose }, ref) => {
const mx = useMatrixClient();
- const { hashRouter } = useClientConfig();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
};
const handleCopyLink = () => {
- copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), linkPath));
+ const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
+ const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
+ copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
requestClose();
};
escapeDeactivates: stopPropagation,
}}
>
- <RoomNavItemMenu
- room={room}
- linkPath={linkPath}
- requestClose={() => setMenuAnchor(undefined)}
- />
+ <RoomNavItemMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
</FocusTrap>
}
>
toRem,
} from 'folds';
import { isKeyHotkey } from 'is-hotkey';
+import { Opts as LinkifyOpts } from 'linkifyjs';
import {
decryptFile,
eventWithShortcode,
factoryEventSentBy,
getMxIdLocalPart,
- isRoomId,
- isUserId,
} from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
ImageContent,
EventContent,
} from '../../components/message';
-import { getReactCustomHtmlParser } from '../../plugins/react-custom-html-parser';
+import {
+ factoryRenderLinkifyWithMention,
+ getReactCustomHtmlParser,
+ LINKIFY_OPTS,
+ makeMentionCustomProps,
+ renderMatrixMention,
+} from '../../plugins/react-custom-html-parser';
import {
canEditEvent,
decryptAllTimelineEvent,
} from '../../utils/room';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
-import { openJoinAlias, openProfileViewer } from '../../../client/action/navigation';
+import { openProfileViewer } from '../../../client/action/navigation';
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
import { Reactions, Message, Event, EncryptedContent } from './message';
import { useMemberEventParser } from '../../hooks/useMemberEventParser';
import { RenderMessageContent } from '../../components/RenderMessageContent';
import { Image } from '../../components/media';
import { ImageViewer } from '../../components/image-viewer';
-import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { roomToParentsAtom } from '../../state/room/roomToParents';
import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
+import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
+import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
+import { useRoomNavigate } from '../../hooks/useRoomNavigate';
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => (
const canRedact = canDoAction('redact', myPowerLevel);
const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
const [editId, setEditId] = useState<string>();
- const { navigateRoom, navigateSpace } = useRoomNavigate();
const roomToParents = useAtomValue(roomToParentsAtom);
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
+ const { navigateRoom } = useRoomNavigate();
+ const mentionClickHandler = useMentionClickHandler(room.roomId);
+ const spoilerClickHandler = useSpoilerClickHandler();
const imagePackRooms: Room[] = useMemo(() => {
const allParentSpaces = [room.roomId].concat(
>();
const alive = useAlive();
+ const linkifyOpts = useMemo<LinkifyOpts>(
+ () => ({
+ ...LINKIFY_OPTS,
+ render: factoryRenderLinkifyWithMention((href) =>
+ renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
+ ),
+ }),
+ [mx, room, mentionClickHandler]
+ );
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
() =>
- 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, spoilerClickHandler, mentionClickHandler]
);
const parseMemberEvent = useMemberEventParser();
// so timeline can be updated with evt like: edits, reactions etc
if (atBottomRef.current) {
if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) {
- requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()));
+ requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!));
}
if (document.hasFocus()) {
}, [scrollToElement, editId]);
const handleJumpToLatest = () => {
+ if (eventId) {
+ navigateRoom(room.roomId, undefined, { replace: true });
+ }
setTimeline(getInitialTimeline(room));
scrollToBottomRef.current.count += 1;
scrollToBottomRef.current.smooth = false;
mediaAutoLoad={mediaAutoLoad}
urlPreview={showUrlPreview}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
outlineAttachment={messageLayout === 2}
/>
)}
mediaAutoLoad={mediaAutoLoad}
urlPreview={showUrlPreview}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
outlineAttachment={messageLayout === 2}
/>
);
import * as css from './RoomTombstone.css';
import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { genRoomVia } from '../../../util/matrixUtil';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { Membership } from '../../../types/matrix/room';
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
+import { getViaServers } from '../../plugins/via-servers';
type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string };
export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) {
const [joinState, handleJoin] = useAsyncCallback(
useCallback(() => {
const currentRoom = mx.getRoom(roomId);
- const via = currentRoom ? genRoomVia(currentRoom) : [];
+ const via = currentRoom ? getViaServers(currentRoom) : [];
return mx.joinRoom(replacementRoomId, {
viaServers: via,
});
PopOut,
RectCords,
} from 'folds';
-import { useLocation, useNavigate } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
import { JoinRule, Room } from 'matrix-js-sdk';
import { useAtomValue } from 'jotai';
import { useSetSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { useSpaceOptionally } from '../../hooks/useSpace';
-import {
- getHomeSearchPath,
- getOriginBaseUrl,
- getSpaceSearchPath,
- joinPathComponent,
- withOriginBaseUrl,
- withSearchParam,
-} from '../../pages/pathUtils';
-import { getCanonicalAliasOrRoomId } from '../../utils/matrix';
+import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
+import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
import { _SearchPathSearchParams } from '../../pages/paths';
import * as css from './RoomViewHeader.css';
import { useRoomUnread } from '../../state/hooks/unread';
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList';
-import { useClientConfig } from '../../hooks/useClientConfig';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { stopPropagation } from '../../utils/keyboard';
+import { getMatrixToRoom } from '../../plugins/matrix-to';
+import { getViaServers } from '../../plugins/via-servers';
type RoomMenuProps = {
room: Room;
- linkPath: string;
requestClose: () => void;
};
-const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(
- ({ room, linkPath, requestClose }, ref) => {
- const mx = useMatrixClient();
- const { hashRouter } = useClientConfig();
- const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
- const powerLevels = usePowerLevelsContext();
- const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
- const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
+const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose }, ref) => {
+ const mx = useMatrixClient();
+ const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
+ const powerLevels = usePowerLevelsContext();
+ const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
+ const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
- const handleMarkAsRead = () => {
- markAsRead(mx, room.roomId);
- requestClose();
- };
+ const handleMarkAsRead = () => {
+ markAsRead(mx, room.roomId);
+ requestClose();
+ };
- const handleInvite = () => {
- openInviteUser(room.roomId);
- requestClose();
- };
+ const handleInvite = () => {
+ openInviteUser(room.roomId);
+ requestClose();
+ };
- const handleCopyLink = () => {
- copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), linkPath));
- requestClose();
- };
+ const handleCopyLink = () => {
+ const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
+ const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
+ copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
+ requestClose();
+ };
- const handleRoomSettings = () => {
- toggleRoomSettings(room.roomId);
- requestClose();
- };
+ const handleRoomSettings = () => {
+ toggleRoomSettings(room.roomId);
+ requestClose();
+ };
- return (
- <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
- <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
- <MenuItem
- onClick={handleMarkAsRead}
- size="300"
- after={<Icon size="100" src={Icons.CheckTwice} />}
- radii="300"
- disabled={!unread}
- >
- <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
- Mark as Read
- </Text>
- </MenuItem>
- </Box>
- <Line variant="Surface" size="300" />
- <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
- <MenuItem
- onClick={handleInvite}
- variant="Primary"
- fill="None"
- size="300"
- after={<Icon size="100" src={Icons.UserPlus} />}
- radii="300"
- disabled={!canInvite}
- >
- <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
- Invite
- </Text>
- </MenuItem>
- <MenuItem
- onClick={handleCopyLink}
- size="300"
- after={<Icon size="100" src={Icons.Link} />}
- radii="300"
- >
- <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
- Copy Link
- </Text>
- </MenuItem>
- <MenuItem
- onClick={handleRoomSettings}
- size="300"
- after={<Icon size="100" src={Icons.Setting} />}
- radii="300"
- >
- <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
- Room Settings
- </Text>
- </MenuItem>
- </Box>
- <Line variant="Surface" size="300" />
- <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
- <UseStateProvider initial={false}>
- {(promptLeave, setPromptLeave) => (
- <>
- <MenuItem
- onClick={() => setPromptLeave(true)}
- variant="Critical"
- fill="None"
- size="300"
- after={<Icon size="100" src={Icons.ArrowGoLeft} />}
- radii="300"
- aria-pressed={promptLeave}
- >
- <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
- Leave Room
- </Text>
- </MenuItem>
- {promptLeave && (
- <LeaveRoomPrompt
- roomId={room.roomId}
- onDone={requestClose}
- onCancel={() => setPromptLeave(false)}
- />
- )}
- </>
- )}
- </UseStateProvider>
- </Box>
- </Menu>
- );
- }
-);
+ return (
+ <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
+ <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+ <MenuItem
+ onClick={handleMarkAsRead}
+ size="300"
+ after={<Icon size="100" src={Icons.CheckTwice} />}
+ radii="300"
+ disabled={!unread}
+ >
+ <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
+ Mark as Read
+ </Text>
+ </MenuItem>
+ </Box>
+ <Line variant="Surface" size="300" />
+ <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+ <MenuItem
+ onClick={handleInvite}
+ variant="Primary"
+ fill="None"
+ size="300"
+ after={<Icon size="100" src={Icons.UserPlus} />}
+ radii="300"
+ disabled={!canInvite}
+ >
+ <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
+ Invite
+ </Text>
+ </MenuItem>
+ <MenuItem
+ onClick={handleCopyLink}
+ size="300"
+ after={<Icon size="100" src={Icons.Link} />}
+ radii="300"
+ >
+ <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
+ Copy Link
+ </Text>
+ </MenuItem>
+ <MenuItem
+ onClick={handleRoomSettings}
+ size="300"
+ after={<Icon size="100" src={Icons.Setting} />}
+ radii="300"
+ >
+ <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
+ Room Settings
+ </Text>
+ </MenuItem>
+ </Box>
+ <Line variant="Surface" size="300" />
+ <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+ <UseStateProvider initial={false}>
+ {(promptLeave, setPromptLeave) => (
+ <>
+ <MenuItem
+ onClick={() => setPromptLeave(true)}
+ variant="Critical"
+ fill="None"
+ size="300"
+ after={<Icon size="100" src={Icons.ArrowGoLeft} />}
+ radii="300"
+ aria-pressed={promptLeave}
+ >
+ <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
+ Leave Room
+ </Text>
+ </MenuItem>
+ {promptLeave && (
+ <LeaveRoomPrompt
+ roomId={room.roomId}
+ onDone={requestClose}
+ onCancel={() => setPromptLeave(false)}
+ />
+ )}
+ </>
+ )}
+ </UseStateProvider>
+ </Box>
+ </Menu>
+ );
+});
export function RoomViewHeader() {
const navigate = useNavigate();
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 = {
escapeDeactivates: stopPropagation,
}}
>
- <RoomMenu
- room={room}
- linkPath={currentPath}
- requestClose={() => setMenuAnchor(undefined)}
- />
+ <RoomMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
</FocusTrap>
}
/>
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';
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;
}
>(({ 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?.();
};
--- /dev/null
+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;
+};
--- /dev/null
+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<HTMLElement> => {
+ const mx = useMatrixClient();
+ const { navigateRoom, navigateSpace } = useRoomNavigate();
+ const navigate = useNavigate();
+
+ const handleClick: ReactEventHandler<HTMLElement> = 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;
+};
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 {
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) => {
);
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 {
--- /dev/null
+import { ReactEventHandler, useCallback } from 'react';
+
+export const useSpoilerClickHandler = (): ReactEventHandler<HTMLElement> => {
+ const handleClick: ReactEventHandler<HTMLElement> = 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;
+};
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';
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);
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));
}
} 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';
}
>
{mobile ? null : <Route index element={<WelcomePage />} />}
- <Route path={_CREATE_PATH} element={<p>create</p>} />
+ <Route path={_CREATE_PATH} element={<DirectCreate />} />
<Route
path={_ROOM_PATH}
element={
--- /dev/null
+import React, { useEffect } from 'react';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+import { WelcomePage } from '../WelcomePage';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { getDirectCreateSearchParams } from '../../pathSearchParam';
+import { getDirectPath, getDirectRoomPath } from '../../pathUtils';
+import { getDMRoomFor } from '../../../utils/matrix';
+import { openInviteUser } from '../../../../client/action/navigation';
+import { useDirectRooms } from './useDirectRooms';
+
+export function DirectCreate() {
+ const mx = useMatrixClient();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const { userId } = getDirectCreateSearchParams(searchParams);
+ const directs = useDirectRooms();
+
+ useEffect(() => {
+ 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 <WelcomePage />;
+}
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 <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} />;
+ return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} eventId={eventId} />;
}
return (
export * from './Direct';
export * from './RoomProvider';
+export * from './DirectCreate';
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 <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} />;
+ return (
+ <JoinBeforeNavigate
+ roomIdOrAlias={roomIdOrAlias!}
+ eventId={eventId}
+ viaServers={viaServers}
+ />
+ );
}
return (
} 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';
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';
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;
}: 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<LinkifyOpts>(
+ () => ({
+ ...LINKIFY_OPTS,
+ render: factoryRenderLinkifyWithMention((href) =>
+ renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
+ ),
+ }),
+ [mx, room, mentionClickHandler]
+ );
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
() =>
- 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]>(
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
outlineAttachment
/>
);
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
/>
);
}
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,
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 {
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';
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;
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
({ room, requestClose, onUnpin }, ref) => {
const mx = useMatrixClient();
- const { hashRouter } = useClientConfig();
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
};
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();
};
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();
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);
!allRooms.includes(room.roomId) ||
!getAllParents(roomToParents, room.roomId).has(space.roomId)
) {
- return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} />;
+ return (
+ <JoinBeforeNavigate
+ roomIdOrAlias={roomIdOrAlias!}
+ eventId={eventId}
+ viaServers={viaServers}
+ />
+ );
}
return (
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,
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;
};
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClose }, ref) => {
const mx = useMatrixClient();
- const { hashRouter } = useClientConfig();
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
};
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();
};
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;
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 <JoinBeforeNavigate roomIdOrAlias={spaceIdOrAlias ?? ''} />;
+ return <JoinBeforeNavigate roomIdOrAlias={spaceIdOrAlias ?? ''} viaServers={viaServers} />;
}
return (
--- /dev/null
+import { _RoomSearchParams, DirectCreateSearchParams } from './paths';
+
+type SearchParamsGetter<T> = (searchParams: URLSearchParams) => T;
+
+export const getRoomSearchParams: SearchParamsGetter<_RoomSearchParams> = (searchParams) => ({
+ viaServers: searchParams.get('viaServers') ?? undefined,
+});
+
+export const getDirectCreateSearchParams: SearchParamsGetter<DirectCreateSearchParams> = (
+ searchParams
+) => ({
+ userId: searchParams.get('userId') ?? undefined,
+});
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/';
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}`;
--- /dev/null
+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,
+ };
+};
/* 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,
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'));
ignoreTags: ['span'],
};
+export const makeMentionCustomProps = (
+ handleMentionClick?: ReactEventHandler<HTMLElement>
+): 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 (
+ <a
+ href={href}
+ {...customProps}
+ className={css.Mention({ highlight: mx.getUserId() === userId })}
+ data-mention-id={userId}
+ >
+ {`@${
+ (currentRoom && getMemberDisplayName(currentRoom, userId)) ?? getMxIdLocalPart(userId)
+ }`}
+ </a>
+ );
+ }
+
+ const matrixToRoom = parseMatrixToRoom(href);
+ if (matrixToRoom) {
+ const { roomIdOrAlias, viaServers } = matrixToRoom;
+ const mentionRoom = mx.getRoom(
+ isRoomAlias(roomIdOrAlias) ? getCanonicalAliasRoomId(mx, roomIdOrAlias) : roomIdOrAlias
+ );
+
+ return (
+ <a
+ href={href}
+ {...customProps}
+ className={css.Mention({
+ highlight: currentRoomId === (mentionRoom?.roomId ?? roomIdOrAlias),
+ })}
+ data-mention-id={mentionRoom?.roomId ?? roomIdOrAlias}
+ data-mention-via={viaServers?.join(',')}
+ >
+ {mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias}
+ </a>
+ );
+ }
+
+ const matrixToRoomEvent = parseMatrixToRoomEvent(href);
+ if (matrixToRoomEvent) {
+ const { roomIdOrAlias, eventId, viaServers } = matrixToRoomEvent;
+ const mentionRoom = mx.getRoom(
+ isRoomAlias(roomIdOrAlias) ? getCanonicalAliasRoomId(mx, roomIdOrAlias) : roomIdOrAlias
+ );
+
+ return (
+ <a
+ href={href}
+ {...customProps}
+ className={css.Mention({
+ highlight: currentRoomId === (mentionRoom?.roomId ?? roomIdOrAlias),
+ })}
+ data-mention-id={mentionRoom?.roomId ?? roomIdOrAlias}
+ data-mention-event-id={eventId}
+ data-mention-via={viaServers?.join(',')}
+ >
+ Message: {mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias}
+ </a>
+ );
+ }
+
+ 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 <a {...attributes}>{content}</a>;
+ };
+ return render;
+};
+
export const scaleSystemEmoji = (text: string): (string | JSX.Element)[] =>
findAndReplace(
text,
export const getReactCustomHtmlParser = (
mx: MatrixClient,
- room: Room,
+ roomId: string | undefined,
params: {
+ linkifyOpts: LinkifyOpts;
highlightRegex?: RegExp;
handleSpoilerClick?: ReactEventHandler<HTMLElement>;
handleMentionClick?: ReactEventHandler<HTMLElement>;
}
}
- 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 (
- <span
- {...props}
- className={css.Mention({
- highlight: room.roomId === (mentionRoom?.roomId ?? mentionId),
- })}
- data-mention-id={mentionRoom?.roomId ?? mentionId}
- data-mention-href={props.href}
- role="button"
- tabIndex={params.handleMentionClick ? 0 : -1}
- onKeyDown={params.handleMentionClick}
- onClick={params.handleMentionClick}
- style={{ cursor: 'pointer' }}
- >
- {domToReact(children, opts)}
- </span>
- );
- }
- if (mentionPrefix === '@')
- return (
- <span
- {...props}
- className={css.Mention({ highlight: mx.getUserId() === mentionId })}
- data-mention-id={mentionId}
- data-mention-href={props.href}
- role="button"
- tabIndex={params.handleMentionClick ? 0 : -1}
- onKeyDown={params.handleMentionClick}
- onClick={params.handleMentionClick}
- style={{ cursor: 'pointer' }}
- >
- {`@${getMemberDisplayName(room, mentionId) ?? getMxIdLocalPart(mentionId)}`}
- </span>
- );
- }
+ if (mention) return mention;
}
if (name === 'span' && 'data-mx-spoiler' in props) {
}
if (linkify) {
- return <Linkify options={LINKIFY_OPTS}>{jsx}</Linkify>;
+ return <Linkify options={params.linkifyOpts}>{jsx}</Linkify>;
}
return jsx;
}
--- /dev/null
+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<IPowerLevels>();
+
+ 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<string, number> => {
+ const members = room.getMembers();
+ const serverToPop: Record<string, number> = {};
+
+ 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));
+};
}
};
-export const onEnterOrSpace = (callback: () => void) => (evt: KeyboardEventLike) => {
- if (isKeyHotkey('enter', evt) || isKeyHotkey('space', evt)) {
- evt.preventDefault();
- callback();
- }
-};
+export const onEnterOrSpace =
+ <T>(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();
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;
}[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());