mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-11 17:50:29 +03:00
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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue