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:
parent
74dc76e22e
commit
5058136737
38 changed files with 781 additions and 476 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { MsgType } from 'matrix-js-sdk';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import { Opts } from 'linkifyjs';
|
||||
import {
|
||||
AudioContent,
|
||||
DownloadFile,
|
||||
|
|
@ -27,6 +28,7 @@ import { Image, MediaControl, Video } from './media';
|
|||
import { ImageViewer } from './image-viewer';
|
||||
import { PdfViewer } from './Pdf-viewer';
|
||||
import { TextViewer } from './text-viewer';
|
||||
import { testMatrixTo } from '../plugins/matrix-to';
|
||||
|
||||
type RenderMessageContentProps = {
|
||||
displayName: string;
|
||||
|
|
@ -38,6 +40,7 @@ type RenderMessageContentProps = {
|
|||
urlPreview?: boolean;
|
||||
highlightRegex?: RegExp;
|
||||
htmlReactParserOptions: HTMLReactParserOptions;
|
||||
linkifyOpts: Opts;
|
||||
outlineAttachment?: boolean;
|
||||
};
|
||||
export function RenderMessageContent({
|
||||
|
|
@ -50,8 +53,21 @@ export function RenderMessageContent({
|
|||
urlPreview,
|
||||
highlightRegex,
|
||||
htmlReactParserOptions,
|
||||
linkifyOpts,
|
||||
outlineAttachment,
|
||||
}: RenderMessageContentProps) {
|
||||
const renderUrlsPreview = (urls: string[]) => {
|
||||
const filteredUrls = urls.filter((url) => !testMatrixTo(url));
|
||||
if (filteredUrls.length === 0) return undefined;
|
||||
return (
|
||||
<UrlPreviewHolder>
|
||||
{filteredUrls.map((url) => (
|
||||
<UrlPreviewCard key={url} url={url} ts={ts} />
|
||||
))}
|
||||
</UrlPreviewHolder>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFile = () => (
|
||||
<MFile
|
||||
content={getContent()}
|
||||
|
|
@ -95,19 +111,10 @@ export function RenderMessageContent({
|
|||
{...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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -123,19 +130,10 @@ export function RenderMessageContent({
|
|||
{...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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -150,19 +148,10 @@ export function RenderMessageContent({
|
|||
{...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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { mDirectAtom } from '../../../state/mDirectList';
|
|||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { factoryRoomIdByActivity } from '../../../utils/sort';
|
||||
import { RoomAvatar, RoomIcon } from '../../room-avatar';
|
||||
import { getViaServers } from '../../../plugins/via-servers';
|
||||
|
||||
type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void;
|
||||
|
||||
|
|
@ -104,10 +105,14 @@ export function RoomMentionAutocomplete({
|
|||
}, [query.text, search, resetSearch]);
|
||||
|
||||
const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => {
|
||||
const mentionRoom = mx.getRoom(roomAliasOrId);
|
||||
const viaServers = mentionRoom ? getViaServers(mentionRoom) : undefined;
|
||||
const mentionEl = createMentionElement(
|
||||
roomAliasOrId,
|
||||
name.startsWith('#') ? name : `#${name}`,
|
||||
roomId === roomAliasOrId || mx.getRoom(roomId)?.getCanonicalAlias() === roomAliasOrId
|
||||
roomId === roomAliasOrId || mx.getRoom(roomId)?.getCanonicalAlias() === roomAliasOrId,
|
||||
undefined,
|
||||
viaServers
|
||||
);
|
||||
replaceWithElement(editor, query.range, mentionEl);
|
||||
moveCursor(editor, true);
|
||||
|
|
|
|||
|
|
@ -18,8 +18,13 @@ import {
|
|||
ParagraphElement,
|
||||
UnorderedListElement,
|
||||
} from './slate';
|
||||
import { parseMatrixToUrl } from '../../utils/matrix';
|
||||
import { createEmoticonElement, createMentionElement } from './utils';
|
||||
import {
|
||||
parseMatrixToRoom,
|
||||
parseMatrixToRoomEvent,
|
||||
parseMatrixToUser,
|
||||
testMatrixTo,
|
||||
} from '../../plugins/matrix-to';
|
||||
|
||||
const markNodeToType: Record<string, MarkType> = {
|
||||
b: MarkType.Bold,
|
||||
|
|
@ -68,11 +73,33 @@ const elementToInlineNode = (node: Element): MentionElement | EmoticonElement |
|
|||
return createEmoticonElement(src, alt || 'Unknown Emoji');
|
||||
}
|
||||
if (node.name === 'a') {
|
||||
const { href } = node.attribs;
|
||||
const href = decodeURIComponent(node.attribs.href);
|
||||
if (typeof href !== 'string') return undefined;
|
||||
const [mxId] = parseMatrixToUrl(href);
|
||||
if (mxId) {
|
||||
return createMentionElement(mxId, parseNodeText(node) || mxId, false);
|
||||
if (testMatrixTo(href)) {
|
||||
const userMention = parseMatrixToUser(href);
|
||||
if (userMention) {
|
||||
return createMentionElement(userMention, parseNodeText(node) || userMention, false);
|
||||
}
|
||||
const roomMention = parseMatrixToRoom(href);
|
||||
if (roomMention) {
|
||||
return createMentionElement(
|
||||
roomMention.roomIdOrAlias,
|
||||
parseNodeText(node) || roomMention.roomIdOrAlias,
|
||||
false,
|
||||
undefined,
|
||||
roomMention.viaServers
|
||||
);
|
||||
}
|
||||
const eventMention = parseMatrixToRoomEvent(href);
|
||||
if (eventMention) {
|
||||
return createMentionElement(
|
||||
eventMention.roomIdOrAlias,
|
||||
parseNodeText(node) || eventMention.roomIdOrAlias,
|
||||
false,
|
||||
eventMention.eventId,
|
||||
eventMention.viaServers
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -51,10 +51,19 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
|
|||
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(
|
||||
|
|
|
|||
2
src/app/components/editor/slate.d.ts
vendored
2
src/app/components/editor/slate.d.ts
vendored
|
|
@ -29,6 +29,8 @@ export type LinkElement = {
|
|||
export type MentionElement = {
|
||||
type: BlockType.Mention;
|
||||
id: string;
|
||||
eventId?: string;
|
||||
viaServers?: string[];
|
||||
highlight: boolean;
|
||||
name: string;
|
||||
children: Text[];
|
||||
|
|
|
|||
|
|
@ -158,10 +158,14 @@ export const resetEditorHistory = (editor: Editor) => {
|
|||
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: '' }],
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
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;
|
||||
|
|
@ -15,12 +12,14 @@ type RenderBodyProps = {
|
|||
|
||||
highlightRegex?: RegExp;
|
||||
htmlReactParserOptions: HTMLReactParserOptions;
|
||||
linkifyOpts: Opts;
|
||||
};
|
||||
export function RenderBody({
|
||||
body,
|
||||
customBody,
|
||||
highlightRegex,
|
||||
htmlReactParserOptions,
|
||||
linkifyOpts,
|
||||
}: RenderBodyProps) {
|
||||
if (body === '') <MessageEmptyContent />;
|
||||
if (customBody) {
|
||||
|
|
@ -28,7 +27,7 @@ export function RenderBody({
|
|||
return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions);
|
||||
}
|
||||
return (
|
||||
<Linkify options={LINKIFY_OPTS}>
|
||||
<Linkify options={linkifyOpts}>
|
||||
{highlightRegex
|
||||
? highlightText(highlightRegex, scaleSystemEmoji(body))
|
||||
: scaleSystemEmoji(body)}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ type RoomCardProps = {
|
|||
topic?: string;
|
||||
memberCount?: number;
|
||||
roomType?: string;
|
||||
viaServers?: string[];
|
||||
onView?: (roomId: string) => void;
|
||||
renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode;
|
||||
};
|
||||
|
|
@ -152,6 +153,7 @@ export const RoomCard = as<'div', RoomCardProps>(
|
|||
topic,
|
||||
memberCount,
|
||||
roomType,
|
||||
viaServers,
|
||||
onView,
|
||||
renderTopicViewer,
|
||||
...props
|
||||
|
|
@ -194,7 +196,7 @@ export const RoomCard = as<'div', RoomCardProps>(
|
|||
);
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -9,8 +9,12 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
|||
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();
|
||||
|
|
@ -20,7 +24,7 @@ export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) {
|
|||
navigateSpace(roomId);
|
||||
return;
|
||||
}
|
||||
navigateRoom(roomId);
|
||||
navigateRoom(roomId, eventId);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -46,6 +50,7 @@ export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) {
|
|||
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} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ import React, { MouseEventHandler, useMemo } from 'react';
|
|||
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 {
|
||||
|
|
@ -31,8 +35,9 @@ import { getMemberAvatarMxc, getMemberDisplayName, getRoomAvatarUrl } from '../.
|
|||
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;
|
||||
|
|
@ -51,38 +56,29 @@ export function SearchResultGroup({
|
|||
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]>(
|
||||
|
|
@ -101,6 +97,7 @@ export function SearchResultGroup({
|
|||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={urlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
highlightRegex={highlightRegex}
|
||||
outlineAttachment
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -28,25 +28,24 @@ import { useRoomUnread } from '../../state/hooks/unread';
|
|||
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);
|
||||
|
|
@ -63,7 +62,9 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
|||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
|
|
@ -273,11 +274,7 @@ export function RoomNavItem({
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<RoomNavItemMenu
|
||||
room={room}
|
||||
linkPath={linkPath}
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
/>
|
||||
<RoomNavItemMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -45,13 +45,12 @@ import {
|
|||
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';
|
||||
|
|
@ -70,7 +69,13 @@ import {
|
|||
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,
|
||||
|
|
@ -85,7 +90,7 @@ import {
|
|||
} 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';
|
||||
|
|
@ -109,10 +114,12 @@ import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
|
|||
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) => (
|
||||
|
|
@ -445,9 +452,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
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(
|
||||
|
|
@ -487,34 +496,23 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
>();
|
||||
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();
|
||||
|
||||
|
|
@ -597,7 +595,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
// 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()) {
|
||||
|
|
@ -819,6 +817,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
}, [scrollToElement, editId]);
|
||||
|
||||
const handleJumpToLatest = () => {
|
||||
if (eventId) {
|
||||
navigateRoom(room.roomId, undefined, { replace: true });
|
||||
}
|
||||
setTimeline(getInitialTimeline(room));
|
||||
scrollToBottomRef.current.count += 1;
|
||||
scrollToBottomRef.current.smooth = false;
|
||||
|
|
@ -1036,6 +1037,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={showUrlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment={messageLayout === 2}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -1132,6 +1134,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={showUrlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment={messageLayout === 2}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { Box, Button, Spinner, Text, color } from 'folds';
|
|||
|
||||
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) {
|
||||
|
|
@ -17,7 +17,7 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone
|
|||
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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {
|
|||
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';
|
||||
|
||||
|
|
@ -35,15 +35,8 @@ import { useRoom } from '../../hooks/useRoom';
|
|||
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';
|
||||
|
|
@ -55,128 +48,127 @@ import { copyToClipboard } from '../../utils/dom';
|
|||
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();
|
||||
|
|
@ -195,8 +187,6 @@ export function RoomViewHeader() {
|
|||
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined;
|
||||
|
||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const location = useLocation();
|
||||
const currentPath = joinPathComponent(location);
|
||||
|
||||
const handleSearchClick = () => {
|
||||
const searchParams: _SearchPathSearchParams = {
|
||||
|
|
@ -336,11 +326,7 @@ export function RoomViewHeader() {
|
|||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<RoomMenu
|
||||
room={room}
|
||||
linkPath={currentPath}
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
/>
|
||||
<RoomMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ import {
|
|||
getMemberAvatarMxc,
|
||||
getMemberDisplayName,
|
||||
} from '../../../utils/room';
|
||||
import { getCanonicalAliasOrRoomId, getMxIdLocalPart } from '../../../utils/matrix';
|
||||
import { getCanonicalAliasOrRoomId, getMxIdLocalPart, isRoomAlias } from '../../../utils/matrix';
|
||||
import { MessageLayout, MessageSpacing } from '../../../state/settings';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||
|
|
@ -63,18 +63,10 @@ import { EmojiBoard } from '../../../components/emoji-board';
|
|||
import { ReactionViewer } from '../reaction-viewer';
|
||||
import { MessageEditor } from './MessageEditor';
|
||||
import { UserAvatar } from '../../../components/user-avatar';
|
||||
import { useSpaceOptionally } from '../../../hooks/useSpace';
|
||||
import { useDirectSelected } from '../../../hooks/router/useDirectSelected';
|
||||
import {
|
||||
getDirectRoomPath,
|
||||
getHomeRoomPath,
|
||||
getOriginBaseUrl,
|
||||
getSpaceRoomPath,
|
||||
withOriginBaseUrl,
|
||||
} from '../../../pages/pathUtils';
|
||||
import { copyToClipboard } from '../../../utils/dom';
|
||||
import { useClientConfig } from '../../../hooks/useClientConfig';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { getMatrixToRoomEvent } from '../../../plugins/matrix-to';
|
||||
import { getViaServers } from '../../../plugins/via-servers';
|
||||
|
||||
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
|
||||
|
||||
|
|
@ -321,23 +313,13 @@ export const MessageCopyLinkItem = as<
|
|||
}
|
||||
>(({ room, mEvent, onClose, ...props }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const { hashRouter } = useClientConfig();
|
||||
const space = useSpaceOptionally();
|
||||
const directSelected = useDirectSelected();
|
||||
|
||||
const handleCopy = () => {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||
let eventPath = getHomeRoomPath(roomIdOrAlias, mEvent.getId());
|
||||
if (space) {
|
||||
eventPath = getSpaceRoomPath(
|
||||
getCanonicalAliasOrRoomId(mx, space.roomId),
|
||||
roomIdOrAlias,
|
||||
mEvent.getId()
|
||||
);
|
||||
} else if (directSelected) {
|
||||
eventPath = getDirectRoomPath(roomIdOrAlias, mEvent.getId());
|
||||
}
|
||||
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), eventPath));
|
||||
const eventId = mEvent.getId();
|
||||
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
||||
if (!eventId) return;
|
||||
copyToClipboard(getMatrixToRoomEvent(roomIdOrAlias, eventId, viaServers));
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
|
|
|
|||
14
src/app/hooks/router/useSearchParamsViaServers.ts
Normal file
14
src/app/hooks/router/useSearchParamsViaServers.ts
Normal 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;
|
||||
};
|
||||
43
src/app/hooks/useMentionClickHandler.ts
Normal file
43
src/app/hooks/useMentionClickHandler.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { NavigateOptions, useNavigate } from 'react-router-dom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { getCanonicalAliasOrRoomId } from '../utils/matrix';
|
||||
import {
|
||||
|
|
@ -12,12 +12,14 @@ import { useMatrixClient } from './useMatrixClient';
|
|||
import { getOrphanParents } from '../utils/room';
|
||||
import { roomToParentsAtom } from '../state/room/roomToParents';
|
||||
import { mDirectAtom } from '../state/mDirectList';
|
||||
import { useSelectedSpace } from './router/useSelectedSpace';
|
||||
|
||||
export const useRoomNavigate = () => {
|
||||
const navigate = useNavigate();
|
||||
const mx = useMatrixClient();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const spaceSelectedId = useSelectedSpace();
|
||||
|
||||
const navigateSpace = useCallback(
|
||||
(roomId: string) => {
|
||||
|
|
@ -28,24 +30,29 @@ export const useRoomNavigate = () => {
|
|||
);
|
||||
|
||||
const navigateRoom = useCallback(
|
||||
(roomId: string, eventId?: string) => {
|
||||
(roomId: string, eventId?: string, opts?: NavigateOptions) => {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
|
||||
|
||||
const orphanParents = getOrphanParents(roomToParents, roomId);
|
||||
if (orphanParents.length > 0) {
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, orphanParents[0]);
|
||||
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId));
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
|
||||
mx,
|
||||
spaceSelectedId && orphanParents.includes(spaceSelectedId)
|
||||
? spaceSelectedId
|
||||
: orphanParents[0]
|
||||
);
|
||||
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mDirects.has(roomId)) {
|
||||
navigate(getDirectRoomPath(roomIdOrAlias, eventId));
|
||||
navigate(getDirectRoomPath(roomIdOrAlias, eventId), opts);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(getHomeRoomPath(roomIdOrAlias, eventId));
|
||||
navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
|
||||
},
|
||||
[mx, navigate, roomToParents, mDirects]
|
||||
[mx, navigate, spaceSelectedId, roomToParents, mDirects]
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
14
src/app/hooks/useSpoilerClickHandler.ts
Normal file
14
src/app/hooks/useSpoilerClickHandler.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -5,7 +5,7 @@ import './SpaceAddExisting.scss';
|
|||
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { joinRuleToIconSrc, getIdServer, genRoomVia } from '../../../util/matrixUtil';
|
||||
import { joinRuleToIconSrc, getIdServer } from '../../../util/matrixUtil';
|
||||
import { Debounce } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
|
|
@ -27,6 +27,7 @@ import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList';
|
|||
import { allRoomsAtom } from '../../state/room-list/roomList';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { getViaServers } from '../../plugins/via-servers';
|
||||
|
||||
function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
|
||||
const mountStore = useStore(roomId);
|
||||
|
|
@ -69,7 +70,7 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
|
|||
|
||||
const promises = selected.map((rId) => {
|
||||
const room = mx.getRoom(rId);
|
||||
const via = genRoomVia(room);
|
||||
const via = getViaServers(room);
|
||||
if (via.length === 0) {
|
||||
via.push(getIdServer(rId));
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue