imnyang/cinny
Watch
1
0
Fork
You've already forked cinny
0

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
This commit is contained in:
Ajay Bura 2024-07-30 17:48:59 +05:30 • committed by GitHub
commit 5058136737
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 781 additions and 476 deletions

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { MsgType } from 'matrix-js-sdk'; import { MsgType } from 'matrix-js-sdk';
import { HTMLReactParserOptions } from 'html-react-parser'; import { HTMLReactParserOptions } from 'html-react-parser';
import { Opts } from 'linkifyjs';
import { import {
AudioContent, AudioContent,
DownloadFile, DownloadFile,
@ -27,6 +28,7 @@ import { Image, MediaControl, Video } from './media';
import { ImageViewer } from './image-viewer'; import { ImageViewer } from './image-viewer';
import { PdfViewer } from './Pdf-viewer'; import { PdfViewer } from './Pdf-viewer';
import { TextViewer } from './text-viewer'; import { TextViewer } from './text-viewer';
import { testMatrixTo } from '../plugins/matrix-to';
type RenderMessageContentProps = { type RenderMessageContentProps = {
displayName: string; displayName: string;
@ -38,6 +40,7 @@ type RenderMessageContentProps = {
urlPreview?: boolean; urlPreview?: boolean;
highlightRegex?: RegExp; highlightRegex?: RegExp;
htmlReactParserOptions: HTMLReactParserOptions; htmlReactParserOptions: HTMLReactParserOptions;
linkifyOpts: Opts;
outlineAttachment?: boolean; outlineAttachment?: boolean;
}; };
export function RenderMessageContent({ export function RenderMessageContent({
@ -50,8 +53,21 @@ export function RenderMessageContent({
urlPreview, urlPreview,
highlightRegex, highlightRegex,
htmlReactParserOptions, htmlReactParserOptions,
linkifyOpts,
outlineAttachment, outlineAttachment,
}: RenderMessageContentProps) { }: 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 = () => ( const renderFile = () => (
<MFile <MFile
content={getContent()} content={getContent()}
@ -95,19 +111,10 @@ export function RenderMessageContent({
{...props} {...props}
highlightRegex={highlightRegex} highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/> />
)} )}
renderUrlsPreview={ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
urlPreview
? (urls) => (
<UrlPreviewHolder>
{urls.map((url) => (
<UrlPreviewCard key={url} url={url} ts={ts} />
))}
</UrlPreviewHolder>
)
: undefined
}
/> />
); );
} }
@ -123,19 +130,10 @@ export function RenderMessageContent({
{...props} {...props}
highlightRegex={highlightRegex} highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/> />
)} )}
renderUrlsPreview={ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
urlPreview
? (urls) => (
<UrlPreviewHolder>
{urls.map((url) => (
<UrlPreviewCard key={url} url={url} ts={ts} />
))}
</UrlPreviewHolder>
)
: undefined
}
/> />
); );
} }
@ -150,19 +148,10 @@ export function RenderMessageContent({
{...props} {...props}
highlightRegex={highlightRegex} highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/> />
)} )}
renderUrlsPreview={ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
urlPreview
? (urls) => (
<UrlPreviewHolder>
{urls.map((url) => (
<UrlPreviewCard key={url} url={url} ts={ts} />
))}
</UrlPreviewHolder>
)
: undefined
}
/> />
); );
} }

View file

@ -17,6 +17,7 @@ import { mDirectAtom } from '../../../state/mDirectList';
import { allRoomsAtom } from '../../../state/room-list/roomList'; import { allRoomsAtom } from '../../../state/room-list/roomList';
import { factoryRoomIdByActivity } from '../../../utils/sort'; import { factoryRoomIdByActivity } from '../../../utils/sort';
import { RoomAvatar, RoomIcon } from '../../room-avatar'; import { RoomAvatar, RoomIcon } from '../../room-avatar';
import { getViaServers } from '../../../plugins/via-servers';
type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void; type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void;
@ -104,10 +105,14 @@ export function RoomMentionAutocomplete({
}, [query.text, search, resetSearch]); }, [query.text, search, resetSearch]);
const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => { const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => {
const mentionRoom = mx.getRoom(roomAliasOrId);
const viaServers = mentionRoom ? getViaServers(mentionRoom) : undefined;
const mentionEl = createMentionElement( const mentionEl = createMentionElement(
roomAliasOrId, roomAliasOrId,
name.startsWith('#') ? name : `#${name}`, 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); replaceWithElement(editor, query.range, mentionEl);
moveCursor(editor, true); moveCursor(editor, true);

View file

@ -18,8 +18,13 @@ import {
ParagraphElement, ParagraphElement,
UnorderedListElement, UnorderedListElement,
} from './slate'; } from './slate';
import { parseMatrixToUrl } from '../../utils/matrix';
import { createEmoticonElement, createMentionElement } from './utils'; import { createEmoticonElement, createMentionElement } from './utils';
import {
parseMatrixToRoom,
parseMatrixToRoomEvent,
parseMatrixToUser,
testMatrixTo,
} from '../../plugins/matrix-to';
const markNodeToType: Record<string, MarkType> = { const markNodeToType: Record<string, MarkType> = {
b: MarkType.Bold, b: MarkType.Bold,
@ -68,11 +73,33 @@ const elementToInlineNode = (node: Element): MentionElement | EmoticonElement |
return createEmoticonElement(src, alt || 'Unknown Emoji'); return createEmoticonElement(src, alt || 'Unknown Emoji');
} }
if (node.name === 'a') { if (node.name === 'a') {
const { href } = node.attribs; const href = decodeURIComponent(node.attribs.href);
if (typeof href !== 'string') return undefined; if (typeof href !== 'string') return undefined;
const [mxId] = parseMatrixToUrl(href); if (testMatrixTo(href)) {
if (mxId) { const userMention = parseMatrixToUser(href);
return createMentionElement(mxId, parseNodeText(node) || mxId, false); 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; return undefined;

View file

@ -51,10 +51,19 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
case BlockType.UnorderedList: case BlockType.UnorderedList:
return `<ul>${children}</ul>`; return `<ul>${children}</ul>`;
case BlockType.Mention: case BlockType.Mention: {
return `<a href="https://matrix.to/#/${encodeURIComponent(node.id)}">${sanitizeText( let fragment = node.id;
node.name
)}</a>`; 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: case BlockType.Emoticon:
return node.key.startsWith('mxc://') return node.key.startsWith('mxc://')
? `<img data-mx-emoticon src="${node.key}" alt="${sanitizeText( ? `<img data-mx-emoticon src="${node.key}" alt="${sanitizeText(

View file

@ -29,6 +29,8 @@ export type LinkElement = {
export type MentionElement = { export type MentionElement = {
type: BlockType.Mention; type: BlockType.Mention;
id: string; id: string;
eventId?: string;
viaServers?: string[];
highlight: boolean; highlight: boolean;
name: string; name: string;
children: Text[]; children: Text[];

View file

@ -158,10 +158,14 @@ export const resetEditorHistory = (editor: Editor) => {
export const createMentionElement = ( export const createMentionElement = (
id: string, id: string,
name: string, name: string,
highlight: boolean highlight: boolean,
eventId?: string,
viaServers?: string[]
): MentionElement => ({ ): MentionElement => ({
type: BlockType.Mention, type: BlockType.Mention,
id, id,
eventId,
viaServers,
highlight, highlight,
name, name,
children: [{ text: '' }], children: [{ text: '' }],

View file

@ -1,13 +1,10 @@
import React from 'react'; import React from 'react';
import parse, { HTMLReactParserOptions } from 'html-react-parser'; import parse, { HTMLReactParserOptions } from 'html-react-parser';
import Linkify from 'linkify-react'; import Linkify from 'linkify-react';
import { Opts } from 'linkifyjs';
import { MessageEmptyContent } from './content'; import { MessageEmptyContent } from './content';
import { sanitizeCustomHtml } from '../../utils/sanitize'; import { sanitizeCustomHtml } from '../../utils/sanitize';
import { import { highlightText, scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
LINKIFY_OPTS,
highlightText,
scaleSystemEmoji,
} from '../../plugins/react-custom-html-parser';
type RenderBodyProps = { type RenderBodyProps = {
body: string; body: string;
@ -15,12 +12,14 @@ type RenderBodyProps = {
highlightRegex?: RegExp; highlightRegex?: RegExp;
htmlReactParserOptions: HTMLReactParserOptions; htmlReactParserOptions: HTMLReactParserOptions;
linkifyOpts: Opts;
}; };
export function RenderBody({ export function RenderBody({
body, body,
customBody, customBody,
highlightRegex, highlightRegex,
htmlReactParserOptions, htmlReactParserOptions,
linkifyOpts,
}: RenderBodyProps) { }: RenderBodyProps) {
if (body === '') <MessageEmptyContent />; if (body === '') <MessageEmptyContent />;
if (customBody) { if (customBody) {
@ -28,7 +27,7 @@ export function RenderBody({
return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions); return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions);
} }
return ( return (
<Linkify options={LINKIFY_OPTS}> <Linkify options={linkifyOpts}>
{highlightRegex {highlightRegex
? highlightText(highlightRegex, scaleSystemEmoji(body)) ? highlightText(highlightRegex, scaleSystemEmoji(body))
: scaleSystemEmoji(body)} : scaleSystemEmoji(body)}

View file

@ -138,6 +138,7 @@ type RoomCardProps = {
topic?: string; topic?: string;
memberCount?: number; memberCount?: number;
roomType?: string; roomType?: string;
viaServers?: string[];
onView?: (roomId: string) => void; onView?: (roomId: string) => void;
renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode; renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode;
}; };
@ -152,6 +153,7 @@ export const RoomCard = as<'div', RoomCardProps>(
topic, topic,
memberCount, memberCount,
roomType, roomType,
viaServers,
onView, onView,
renderTopicViewer, renderTopicViewer,
...props ...props
@ -194,7 +196,7 @@ export const RoomCard = as<'div', RoomCardProps>(
); );
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>( const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
useCallback(() => mx.joinRoom(roomIdOrAlias), [mx, roomIdOrAlias]) useCallback(() => mx.joinRoom(roomIdOrAlias, { viaServers }), [mx, roomIdOrAlias, viaServers])
); );
const joining = const joining =
joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success; joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success;

View file

@ -9,8 +9,12 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../state/room-list/roomList'; import { allRoomsAtom } from '../../state/room-list/roomList';
type JoinBeforeNavigateProps = { roomIdOrAlias: string }; type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] };
export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) { export function JoinBeforeNavigate({
roomIdOrAlias,
eventId,
viaServers,
}: JoinBeforeNavigateProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
const { navigateRoom, navigateSpace } = useRoomNavigate(); const { navigateRoom, navigateSpace } = useRoomNavigate();
@ -20,7 +24,7 @@ export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) {
navigateSpace(roomId); navigateSpace(roomId);
return; return;
} }
navigateRoom(roomId); navigateRoom(roomId, eventId);
}; };
return ( return (
@ -46,6 +50,7 @@ export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) {
topic={summary?.topic} topic={summary?.topic}
memberCount={summary?.num_joined_members} memberCount={summary?.num_joined_members}
roomType={summary?.room_type} roomType={summary?.room_type}
viaServers={viaServers}
renderTopicViewer={(name, topic, requestClose) => ( renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer name={name} topic={topic} requestClose={requestClose} /> <RoomTopicViewer name={name} topic={topic} requestClose={requestClose} />
)} )}

View file

@ -3,13 +3,17 @@ import React, { MouseEventHandler, useMemo } from 'react';
import { IEventWithRoomId, JoinRule, RelationType, Room } from 'matrix-js-sdk'; import { IEventWithRoomId, JoinRule, RelationType, Room } from 'matrix-js-sdk';
import { HTMLReactParserOptions } from 'html-react-parser'; import { HTMLReactParserOptions } from 'html-react-parser';
import { Avatar, Box, Chip, Header, Icon, Icons, Text, config } from 'folds'; import { Avatar, Box, Chip, Header, Icon, Icons, Text, config } from 'folds';
import { Opts as LinkifyOpts } from 'linkifyjs';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { import {
factoryRenderLinkifyWithMention,
getReactCustomHtmlParser, getReactCustomHtmlParser,
LINKIFY_OPTS,
makeHighlightRegex, makeHighlightRegex,
makeMentionCustomProps,
renderMatrixMention,
} from '../../plugins/react-custom-html-parser'; } from '../../plugins/react-custom-html-parser';
import { getMxIdLocalPart, isRoomId, isUserId } from '../../utils/matrix'; import { getMxIdLocalPart } from '../../utils/matrix';
import { openJoinAlias, openProfileViewer } from '../../../client/action/navigation';
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer'; import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room'; import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
import { import {
@ -31,8 +35,9 @@ import { getMemberAvatarMxc, getMemberDisplayName, getRoomAvatarUrl } from '../.
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
import { ResultItem } from './useMessageSearch'; import { ResultItem } from './useMessageSearch';
import { SequenceCard } from '../../components/sequence-card'; import { SequenceCard } from '../../components/sequence-card';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { UserAvatar } from '../../components/user-avatar'; import { UserAvatar } from '../../components/user-avatar';
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
type SearchResultGroupProps = { type SearchResultGroupProps = {
room: Room; room: Room;
@ -51,38 +56,29 @@ export function SearchResultGroup({
onOpen, onOpen,
}: SearchResultGroupProps) { }: SearchResultGroupProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { navigateRoom, navigateSpace } = useRoomNavigate();
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]); 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>( const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
() => () =>
getReactCustomHtmlParser(mx, room, { getReactCustomHtmlParser(mx, room.roomId, {
linkifyOpts,
highlightRegex, highlightRegex,
handleSpoilerClick: (evt) => { handleSpoilerClick: spoilerClickHandler,
const target = evt.currentTarget; handleMentionClick: mentionClickHandler,
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);
},
}), }),
[mx, room, highlightRegex, navigateRoom, navigateSpace] [mx, room, linkifyOpts, highlightRegex, mentionClickHandler, spoilerClickHandler]
); );
const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>( const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>(
@ -101,6 +97,7 @@ export function SearchResultGroup({
mediaAutoLoad={mediaAutoLoad} mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview} urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
highlightRegex={highlightRegex} highlightRegex={highlightRegex}
outlineAttachment outlineAttachment
/> />

View file

@ -28,25 +28,24 @@ import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread'; import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
import { copyToClipboard } from '../../utils/dom'; import { copyToClipboard } from '../../utils/dom';
import { getOriginBaseUrl, withOriginBaseUrl } from '../../pages/pathUtils';
import { markAsRead } from '../../../client/action/notifications'; import { markAsRead } from '../../../client/action/notifications';
import { openInviteUser, toggleRoomSettings } from '../../../client/action/navigation'; import { openInviteUser, toggleRoomSettings } from '../../../client/action/navigation';
import { UseStateProvider } from '../../components/UseStateProvider'; import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveRoomPrompt } from '../../components/leave-room-prompt'; import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
import { useClientConfig } from '../../hooks/useClientConfig';
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers'; import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { TypingIndicator } from '../../components/typing-indicator'; import { TypingIndicator } from '../../components/typing-indicator';
import { stopPropagation } from '../../utils/keyboard'; 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 = { type RoomNavItemMenuProps = {
room: Room; room: Room;
linkPath: string;
requestClose: () => void; requestClose: () => void;
}; };
const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>( const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
({ room, linkPath, requestClose }, ref) => { ({ room, requestClose }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { hashRouter } = useClientConfig();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
@ -63,7 +62,9 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
}; };
const handleCopyLink = () => { 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(); requestClose();
}; };
@ -273,11 +274,7 @@ export function RoomNavItem({
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<RoomNavItemMenu <RoomNavItemMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
room={room}
linkPath={linkPath}
requestClose={() => setMenuAnchor(undefined)}
/>
</FocusTrap> </FocusTrap>
} }
> >

View file

@ -45,13 +45,12 @@ import {
toRem, toRem,
} from 'folds'; } from 'folds';
import { isKeyHotkey } from 'is-hotkey'; import { isKeyHotkey } from 'is-hotkey';
import { Opts as LinkifyOpts } from 'linkifyjs';
import { import {
decryptFile, decryptFile,
eventWithShortcode, eventWithShortcode,
factoryEventSentBy, factoryEventSentBy,
getMxIdLocalPart, getMxIdLocalPart,
isRoomId,
isUserId,
} from '../../utils/matrix'; } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator'; import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
@ -70,7 +69,13 @@ import {
ImageContent, ImageContent,
EventContent, EventContent,
} from '../../components/message'; } 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 { import {
canEditEvent, canEditEvent,
decryptAllTimelineEvent, decryptAllTimelineEvent,
@ -85,7 +90,7 @@ import {
} from '../../utils/room'; } from '../../utils/room';
import { useSetting } from '../../state/hooks/settings'; import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/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 { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
import { Reactions, Message, Event, EncryptedContent } from './message'; import { Reactions, Message, Event, EncryptedContent } from './message';
import { useMemberEventParser } from '../../hooks/useMemberEventParser'; import { useMemberEventParser } from '../../hooks/useMemberEventParser';
@ -109,10 +114,12 @@ import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
import { RenderMessageContent } from '../../components/RenderMessageContent'; import { RenderMessageContent } from '../../components/RenderMessageContent';
import { Image } from '../../components/media'; import { Image } from '../../components/media';
import { ImageViewer } from '../../components/image-viewer'; import { ImageViewer } from '../../components/image-viewer';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { roomToParentsAtom } from '../../state/room/roomToParents'; import { roomToParentsAtom } from '../../state/room/roomToParents';
import { useRoomUnread } from '../../state/hooks/unread'; import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread'; 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>( const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => ( ({ position, className, ...props }, ref) => (
@ -445,9 +452,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const canRedact = canDoAction('redact', myPowerLevel); const canRedact = canDoAction('redact', myPowerLevel);
const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel); const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
const [editId, setEditId] = useState<string>(); const [editId, setEditId] = useState<string>();
const { navigateRoom, navigateSpace } = useRoomNavigate();
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const { navigateRoom } = useRoomNavigate();
const mentionClickHandler = useMentionClickHandler(room.roomId);
const spoilerClickHandler = useSpoilerClickHandler();
const imagePackRooms: Room[] = useMemo(() => { const imagePackRooms: Room[] = useMemo(() => {
const allParentSpaces = [room.roomId].concat( const allParentSpaces = [room.roomId].concat(
@ -487,34 +496,23 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
>(); >();
const alive = useAlive(); 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>( const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
() => () =>
getReactCustomHtmlParser(mx, room, { getReactCustomHtmlParser(mx, room.roomId, {
handleSpoilerClick: (evt) => { linkifyOpts,
const target = evt.currentTarget; handleSpoilerClick: spoilerClickHandler,
if (target.getAttribute('aria-pressed') === 'true') { handleMentionClick: mentionClickHandler,
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);
},
}), }),
[mx, room, navigateRoom, navigateSpace] [mx, room, linkifyOpts, spoilerClickHandler, mentionClickHandler]
); );
const parseMemberEvent = useMemberEventParser(); const parseMemberEvent = useMemberEventParser();
@ -597,7 +595,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
// so timeline can be updated with evt like: edits, reactions etc // so timeline can be updated with evt like: edits, reactions etc
if (atBottomRef.current) { if (atBottomRef.current) {
if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) { if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) {
requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId())); requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!));
} }
if (document.hasFocus()) { if (document.hasFocus()) {
@ -819,6 +817,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
}, [scrollToElement, editId]); }, [scrollToElement, editId]);
const handleJumpToLatest = () => { const handleJumpToLatest = () => {
if (eventId) {
navigateRoom(room.roomId, undefined, { replace: true });
}
setTimeline(getInitialTimeline(room)); setTimeline(getInitialTimeline(room));
scrollToBottomRef.current.count += 1; scrollToBottomRef.current.count += 1;
scrollToBottomRef.current.smooth = false; scrollToBottomRef.current.smooth = false;
@ -1036,6 +1037,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
mediaAutoLoad={mediaAutoLoad} mediaAutoLoad={mediaAutoLoad}
urlPreview={showUrlPreview} urlPreview={showUrlPreview}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
outlineAttachment={messageLayout === 2} outlineAttachment={messageLayout === 2}
/> />
)} )}
@ -1132,6 +1134,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
mediaAutoLoad={mediaAutoLoad} mediaAutoLoad={mediaAutoLoad}
urlPreview={showUrlPreview} urlPreview={showUrlPreview}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
outlineAttachment={messageLayout === 2} outlineAttachment={messageLayout === 2}
/> />
); );

View file

@ -3,11 +3,11 @@ import { Box, Button, Spinner, Text, color } from 'folds';
import * as css from './RoomTombstone.css'; import * as css from './RoomTombstone.css';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { genRoomVia } from '../../../util/matrixUtil';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { Membership } from '../../../types/matrix/room'; import { Membership } from '../../../types/matrix/room';
import { RoomInputPlaceholder } from './RoomInputPlaceholder'; import { RoomInputPlaceholder } from './RoomInputPlaceholder';
import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { getViaServers } from '../../plugins/via-servers';
type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string }; type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string };
export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) { export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) {
@ -17,7 +17,7 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone
const [joinState, handleJoin] = useAsyncCallback( const [joinState, handleJoin] = useAsyncCallback(
useCallback(() => { useCallback(() => {
const currentRoom = mx.getRoom(roomId); const currentRoom = mx.getRoom(roomId);
const via = currentRoom ? genRoomVia(currentRoom) : []; const via = currentRoom ? getViaServers(currentRoom) : [];
return mx.joinRoom(replacementRoomId, { return mx.joinRoom(replacementRoomId, {
viaServers: via, viaServers: via,
}); });

View file

@ -20,7 +20,7 @@ import {
PopOut, PopOut,
RectCords, RectCords,
} from 'folds'; } from 'folds';
import { useLocation, useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { JoinRule, Room } from 'matrix-js-sdk'; import { JoinRule, Room } from 'matrix-js-sdk';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
@ -35,15 +35,8 @@ import { useRoom } from '../../hooks/useRoom';
import { useSetSetting } from '../../state/hooks/settings'; import { useSetSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings'; import { settingsAtom } from '../../state/settings';
import { useSpaceOptionally } from '../../hooks/useSpace'; import { useSpaceOptionally } from '../../hooks/useSpace';
import { import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
getHomeSearchPath, import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
getOriginBaseUrl,
getSpaceSearchPath,
joinPathComponent,
withOriginBaseUrl,
withSearchParam,
} from '../../pages/pathUtils';
import { getCanonicalAliasOrRoomId } from '../../utils/matrix';
import { _SearchPathSearchParams } from '../../pages/paths'; import { _SearchPathSearchParams } from '../../pages/paths';
import * as css from './RoomViewHeader.css'; import * as css from './RoomViewHeader.css';
import { useRoomUnread } from '../../state/hooks/unread'; import { useRoomUnread } from '../../state/hooks/unread';
@ -55,128 +48,127 @@ import { copyToClipboard } from '../../utils/dom';
import { LeaveRoomPrompt } from '../../components/leave-room-prompt'; import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta'; import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList'; import { mDirectAtom } from '../../state/mDirectList';
import { useClientConfig } from '../../hooks/useClientConfig';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { getMatrixToRoom } from '../../plugins/matrix-to';
import { getViaServers } from '../../plugins/via-servers';
type RoomMenuProps = { type RoomMenuProps = {
room: Room; room: Room;
linkPath: string;
requestClose: () => void; requestClose: () => void;
}; };
const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>( const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose }, ref) => {
({ room, linkPath, requestClose }, ref) => { const mx = useMatrixClient();
const mx = useMatrixClient(); const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const { hashRouter } = useClientConfig(); const powerLevels = usePowerLevelsContext();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const powerLevels = usePowerLevelsContext(); const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const handleMarkAsRead = () => { const handleMarkAsRead = () => {
markAsRead(mx, room.roomId); markAsRead(mx, room.roomId);
requestClose(); requestClose();
}; };
const handleInvite = () => { const handleInvite = () => {
openInviteUser(room.roomId); openInviteUser(room.roomId);
requestClose(); requestClose();
}; };
const handleCopyLink = () => { const handleCopyLink = () => {
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), linkPath)); const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
requestClose(); const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
}; copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
requestClose();
};
const handleRoomSettings = () => { const handleRoomSettings = () => {
toggleRoomSettings(room.roomId); toggleRoomSettings(room.roomId);
requestClose(); requestClose();
}; };
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem <MenuItem
onClick={handleMarkAsRead} onClick={handleMarkAsRead}
size="300" size="300"
after={<Icon size="100" src={Icons.CheckTwice} />} after={<Icon size="100" src={Icons.CheckTwice} />}
radii="300" radii="300"
disabled={!unread} disabled={!unread}
> >
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate> <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Mark as Read Mark as Read
</Text> </Text>
</MenuItem> </MenuItem>
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem <MenuItem
onClick={handleInvite} onClick={handleInvite}
variant="Primary" variant="Primary"
fill="None" fill="None"
size="300" size="300"
after={<Icon size="100" src={Icons.UserPlus} />} after={<Icon size="100" src={Icons.UserPlus} />}
radii="300" radii="300"
disabled={!canInvite} disabled={!canInvite}
> >
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate> <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Invite Invite
</Text> </Text>
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={handleCopyLink} onClick={handleCopyLink}
size="300" size="300"
after={<Icon size="100" src={Icons.Link} />} after={<Icon size="100" src={Icons.Link} />}
radii="300" radii="300"
> >
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate> <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Copy Link Copy Link
</Text> </Text>
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={handleRoomSettings} onClick={handleRoomSettings}
size="300" size="300"
after={<Icon size="100" src={Icons.Setting} />} after={<Icon size="100" src={Icons.Setting} />}
radii="300" radii="300"
> >
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate> <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Room Settings Room Settings
</Text> </Text>
</MenuItem> </MenuItem>
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<UseStateProvider initial={false}> <UseStateProvider initial={false}>
{(promptLeave, setPromptLeave) => ( {(promptLeave, setPromptLeave) => (
<> <>
<MenuItem <MenuItem
onClick={() => setPromptLeave(true)} onClick={() => setPromptLeave(true)}
variant="Critical" variant="Critical"
fill="None" fill="None"
size="300" size="300"
after={<Icon size="100" src={Icons.ArrowGoLeft} />} after={<Icon size="100" src={Icons.ArrowGoLeft} />}
radii="300" radii="300"
aria-pressed={promptLeave} aria-pressed={promptLeave}
> >
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate> <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Leave Room Leave Room
</Text> </Text>
</MenuItem> </MenuItem>
{promptLeave && ( {promptLeave && (
<LeaveRoomPrompt <LeaveRoomPrompt
roomId={room.roomId} roomId={room.roomId}
onDone={requestClose} onDone={requestClose}
onCancel={() => setPromptLeave(false)} onCancel={() => setPromptLeave(false)}
/> />
)} )}
</> </>
)} )}
</UseStateProvider> </UseStateProvider>
</Box> </Box>
</Menu> </Menu>
); );
} });
);
export function RoomViewHeader() { export function RoomViewHeader() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -195,8 +187,6 @@ export function RoomViewHeader() {
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined; const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined;
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
const location = useLocation();
const currentPath = joinPathComponent(location);
const handleSearchClick = () => { const handleSearchClick = () => {
const searchParams: _SearchPathSearchParams = { const searchParams: _SearchPathSearchParams = {
@ -336,11 +326,7 @@ export function RoomViewHeader() {
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<RoomMenu <RoomMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
room={room}
linkPath={currentPath}
requestClose={() => setMenuAnchor(undefined)}
/>
</FocusTrap> </FocusTrap>
} }
/> />

View file

@ -51,7 +51,7 @@ import {
getMemberAvatarMxc, getMemberAvatarMxc,
getMemberDisplayName, getMemberDisplayName,
} from '../../../utils/room'; } from '../../../utils/room';
import { getCanonicalAliasOrRoomId, getMxIdLocalPart } from '../../../utils/matrix'; import { getCanonicalAliasOrRoomId, getMxIdLocalPart, isRoomAlias } from '../../../utils/matrix';
import { MessageLayout, MessageSpacing } from '../../../state/settings'; import { MessageLayout, MessageSpacing } from '../../../state/settings';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../../hooks/useRecentEmoji'; import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
@ -63,18 +63,10 @@ import { EmojiBoard } from '../../../components/emoji-board';
import { ReactionViewer } from '../reaction-viewer'; import { ReactionViewer } from '../reaction-viewer';
import { MessageEditor } from './MessageEditor'; import { MessageEditor } from './MessageEditor';
import { UserAvatar } from '../../../components/user-avatar'; 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 { copyToClipboard } from '../../../utils/dom';
import { useClientConfig } from '../../../hooks/useClientConfig';
import { stopPropagation } from '../../../utils/keyboard'; 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; export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
@ -321,23 +313,13 @@ export const MessageCopyLinkItem = as<
} }
>(({ room, mEvent, onClose, ...props }, ref) => { >(({ room, mEvent, onClose, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { hashRouter } = useClientConfig();
const space = useSpaceOptionally();
const directSelected = useDirectSelected();
const handleCopy = () => { const handleCopy = () => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId); const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
let eventPath = getHomeRoomPath(roomIdOrAlias, mEvent.getId()); const eventId = mEvent.getId();
if (space) { const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
eventPath = getSpaceRoomPath( if (!eventId) return;
getCanonicalAliasOrRoomId(mx, space.roomId), copyToClipboard(getMatrixToRoomEvent(roomIdOrAlias, eventId, viaServers));
roomIdOrAlias,
mEvent.getId()
);
} else if (directSelected) {
eventPath = getDirectRoomPath(roomIdOrAlias, mEvent.getId());
}
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), eventPath));
onClose?.(); onClose?.();
}; };

View file

@ -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;
};

View file

@ -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<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;
};

View file

@ -1,5 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { NavigateOptions, useNavigate } from 'react-router-dom';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { getCanonicalAliasOrRoomId } from '../utils/matrix'; import { getCanonicalAliasOrRoomId } from '../utils/matrix';
import { import {
@ -12,12 +12,14 @@ import { useMatrixClient } from './useMatrixClient';
import { getOrphanParents } from '../utils/room'; import { getOrphanParents } from '../utils/room';
import { roomToParentsAtom } from '../state/room/roomToParents'; import { roomToParentsAtom } from '../state/room/roomToParents';
import { mDirectAtom } from '../state/mDirectList'; import { mDirectAtom } from '../state/mDirectList';
import { useSelectedSpace } from './router/useSelectedSpace';
export const useRoomNavigate = () => { export const useRoomNavigate = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const mx = useMatrixClient(); const mx = useMatrixClient();
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
const spaceSelectedId = useSelectedSpace();
const navigateSpace = useCallback( const navigateSpace = useCallback(
(roomId: string) => { (roomId: string) => {
@ -28,24 +30,29 @@ export const useRoomNavigate = () => {
); );
const navigateRoom = useCallback( const navigateRoom = useCallback(
(roomId: string, eventId?: string) => { (roomId: string, eventId?: string, opts?: NavigateOptions) => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId); const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
const orphanParents = getOrphanParents(roomToParents, roomId); const orphanParents = getOrphanParents(roomToParents, roomId);
if (orphanParents.length > 0) { if (orphanParents.length > 0) {
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, orphanParents[0]); const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId)); mx,
spaceSelectedId && orphanParents.includes(spaceSelectedId)
? spaceSelectedId
: orphanParents[0]
);
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
return; return;
} }
if (mDirects.has(roomId)) { if (mDirects.has(roomId)) {
navigate(getDirectRoomPath(roomIdOrAlias, eventId)); navigate(getDirectRoomPath(roomIdOrAlias, eventId), opts);
return; return;
} }
navigate(getHomeRoomPath(roomIdOrAlias, eventId)); navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
}, },
[mx, navigate, roomToParents, mDirects] [mx, navigate, spaceSelectedId, roomToParents, mDirects]
); );
return { return {

View file

@ -0,0 +1,14 @@
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;
};

View file

@ -5,7 +5,7 @@ import './SpaceAddExisting.scss';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation'; 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 { Debounce } from '../../../util/common';
import Text from '../../atoms/text/Text'; 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 { allRoomsAtom } from '../../state/room-list/roomList';
import { mDirectAtom } from '../../state/mDirectList'; import { mDirectAtom } from '../../state/mDirectList';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getViaServers } from '../../plugins/via-servers';
function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) { function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
const mountStore = useStore(roomId); const mountStore = useStore(roomId);
@ -69,7 +70,7 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
const promises = selected.map((rId) => { const promises = selected.map((rId) => {
const room = mx.getRoom(rId); const room = mx.getRoom(rId);
const via = genRoomVia(room); const via = getViaServers(room);
if (via.length === 0) { if (via.length === 0) {
via.push(getIdServer(rId)); via.push(getIdServer(rId));
} }

Some files were not shown because too many files have changed in this diff Show more