mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-15 03:30: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
84
src/app/plugins/matrix-to.ts
Normal file
84
src/app/plugins/matrix-to.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
const MATRIX_TO_BASE = 'https://matrix.to';
|
||||
|
||||
export const getMatrixToUser = (userId: string): string => `${MATRIX_TO_BASE}/#/${userId}`;
|
||||
|
||||
const withViaServers = (fragment: string, viaServers: string[]): string =>
|
||||
`${fragment}?${viaServers.map((server) => `via=${server}`).join('&')}`;
|
||||
|
||||
export const getMatrixToRoom = (roomIdOrAlias: string, viaServers?: string[]): string => {
|
||||
let fragment = roomIdOrAlias;
|
||||
|
||||
if (Array.isArray(viaServers) && viaServers.length > 0) {
|
||||
fragment = withViaServers(fragment, viaServers);
|
||||
}
|
||||
|
||||
return `${MATRIX_TO_BASE}/#/${fragment}`;
|
||||
};
|
||||
|
||||
export const getMatrixToRoomEvent = (
|
||||
roomIdOrAlias: string,
|
||||
eventId: string,
|
||||
viaServers?: string[]
|
||||
): string => {
|
||||
let fragment = `${roomIdOrAlias}/${eventId}`;
|
||||
|
||||
if (Array.isArray(viaServers) && viaServers.length > 0) {
|
||||
fragment = withViaServers(fragment, viaServers);
|
||||
}
|
||||
|
||||
return `${MATRIX_TO_BASE}/#/${fragment}`;
|
||||
};
|
||||
|
||||
export type MatrixToRoom = {
|
||||
roomIdOrAlias: string;
|
||||
viaServers?: string[];
|
||||
};
|
||||
|
||||
export type MatrixToRoomEvent = MatrixToRoom & {
|
||||
eventId: string;
|
||||
};
|
||||
|
||||
const MATRIX_TO = /^https?:\/\/matrix\.to\S*$/;
|
||||
export const testMatrixTo = (href: string): boolean => MATRIX_TO.test(href);
|
||||
|
||||
const MATRIX_TO_USER = /^https?:\/\/matrix\.to\/#\/(@[^:\s]+:[^?/\s]+)\/?$/;
|
||||
const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/?(\?[\S]*)?$/;
|
||||
const MATRIX_TO_ROOM_EVENT =
|
||||
/^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/;
|
||||
|
||||
export const parseMatrixToUser = (href: string): string | undefined => {
|
||||
const match = href.match(MATRIX_TO_USER);
|
||||
if (!match) return undefined;
|
||||
const userId = match[1];
|
||||
return userId;
|
||||
};
|
||||
|
||||
export const parseMatrixToRoom = (href: string): MatrixToRoom | undefined => {
|
||||
const match = href.match(MATRIX_TO_ROOM);
|
||||
if (!match) return undefined;
|
||||
|
||||
const roomIdOrAlias = match[1];
|
||||
const viaSearchStr = match[2];
|
||||
const viaServers = new URLSearchParams(viaSearchStr).getAll('via');
|
||||
|
||||
return {
|
||||
roomIdOrAlias,
|
||||
viaServers: viaServers.length === 0 ? undefined : viaServers,
|
||||
};
|
||||
};
|
||||
|
||||
export const parseMatrixToRoomEvent = (href: string): MatrixToRoomEvent | undefined => {
|
||||
const match = href.match(MATRIX_TO_ROOM_EVENT);
|
||||
if (!match) return undefined;
|
||||
|
||||
const roomIdOrAlias = match[1];
|
||||
const eventId = match[2];
|
||||
const viaSearchStr = match[3];
|
||||
const viaServers = new URLSearchParams(viaSearchStr).getAll('via');
|
||||
|
||||
return {
|
||||
roomIdOrAlias,
|
||||
eventId,
|
||||
viaServers: viaServers.length === 0 ? undefined : viaServers,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import React, { ReactEventHandler, Suspense, lazy } from 'react';
|
||||
import React, { ComponentPropsWithoutRef, ReactEventHandler, Suspense, lazy } from 'react';
|
||||
import {
|
||||
Element,
|
||||
Text as DOMText,
|
||||
|
|
@ -7,18 +7,25 @@ import {
|
|||
attributesToProps,
|
||||
domToReact,
|
||||
} from 'html-react-parser';
|
||||
import { MatrixClient, Room } from 'matrix-js-sdk';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
import { Scroll, Text } from 'folds';
|
||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||
import { IntermediateRepresentation, Opts as LinkifyOpts, OptFn } from 'linkifyjs';
|
||||
import Linkify from 'linkify-react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import * as css from '../styles/CustomHtml.css';
|
||||
import { getMxIdLocalPart, getCanonicalAliasRoomId } from '../utils/matrix';
|
||||
import { getMxIdLocalPart, getCanonicalAliasRoomId, isRoomAlias } from '../utils/matrix';
|
||||
import { getMemberDisplayName } from '../utils/room';
|
||||
import { EMOJI_PATTERN, URL_NEG_LB } from '../utils/regex';
|
||||
import { getHexcodeForEmoji, getShortcodeFor } from './emoji';
|
||||
import { findAndReplace } from '../utils/findAndReplace';
|
||||
import {
|
||||
parseMatrixToRoom,
|
||||
parseMatrixToRoomEvent,
|
||||
parseMatrixToUser,
|
||||
testMatrixTo,
|
||||
} from './matrix-to';
|
||||
import { onEnterOrSpace } from '../utils/keyboard';
|
||||
|
||||
const ReactPrism = lazy(() => import('./react-prism/ReactPrism'));
|
||||
|
||||
|
|
@ -35,6 +42,108 @@ export const LINKIFY_OPTS: LinkifyOpts = {
|
|||
ignoreTags: ['span'],
|
||||
};
|
||||
|
||||
export const makeMentionCustomProps = (
|
||||
handleMentionClick?: ReactEventHandler<HTMLElement>
|
||||
): ComponentPropsWithoutRef<'a'> => ({
|
||||
style: { cursor: 'pointer' },
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener',
|
||||
role: 'link',
|
||||
tabIndex: handleMentionClick ? 0 : -1,
|
||||
onKeyDown: handleMentionClick ? onEnterOrSpace(handleMentionClick) : undefined,
|
||||
onClick: handleMentionClick,
|
||||
});
|
||||
|
||||
export const renderMatrixMention = (
|
||||
mx: MatrixClient,
|
||||
currentRoomId: string | undefined,
|
||||
href: string,
|
||||
customProps: ComponentPropsWithoutRef<'a'>
|
||||
) => {
|
||||
const userId = parseMatrixToUser(href);
|
||||
if (userId) {
|
||||
const currentRoom = mx.getRoom(currentRoomId);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
{...customProps}
|
||||
className={css.Mention({ highlight: mx.getUserId() === userId })}
|
||||
data-mention-id={userId}
|
||||
>
|
||||
{`@${
|
||||
(currentRoom && getMemberDisplayName(currentRoom, userId)) ?? getMxIdLocalPart(userId)
|
||||
}`}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const matrixToRoom = parseMatrixToRoom(href);
|
||||
if (matrixToRoom) {
|
||||
const { roomIdOrAlias, viaServers } = matrixToRoom;
|
||||
const mentionRoom = mx.getRoom(
|
||||
isRoomAlias(roomIdOrAlias) ? getCanonicalAliasRoomId(mx, roomIdOrAlias) : roomIdOrAlias
|
||||
);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
{...customProps}
|
||||
className={css.Mention({
|
||||
highlight: currentRoomId === (mentionRoom?.roomId ?? roomIdOrAlias),
|
||||
})}
|
||||
data-mention-id={mentionRoom?.roomId ?? roomIdOrAlias}
|
||||
data-mention-via={viaServers?.join(',')}
|
||||
>
|
||||
{mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const matrixToRoomEvent = parseMatrixToRoomEvent(href);
|
||||
if (matrixToRoomEvent) {
|
||||
const { roomIdOrAlias, eventId, viaServers } = matrixToRoomEvent;
|
||||
const mentionRoom = mx.getRoom(
|
||||
isRoomAlias(roomIdOrAlias) ? getCanonicalAliasRoomId(mx, roomIdOrAlias) : roomIdOrAlias
|
||||
);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
{...customProps}
|
||||
className={css.Mention({
|
||||
highlight: currentRoomId === (mentionRoom?.roomId ?? roomIdOrAlias),
|
||||
})}
|
||||
data-mention-id={mentionRoom?.roomId ?? roomIdOrAlias}
|
||||
data-mention-event-id={eventId}
|
||||
data-mention-via={viaServers?.join(',')}
|
||||
>
|
||||
Message: {mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const factoryRenderLinkifyWithMention = (
|
||||
mentionRender: (href: string) => JSX.Element | undefined
|
||||
): OptFn<(ir: IntermediateRepresentation) => any> => {
|
||||
const render: OptFn<(ir: IntermediateRepresentation) => any> = ({
|
||||
tagName,
|
||||
attributes,
|
||||
content,
|
||||
}) => {
|
||||
if (tagName === 'a' && testMatrixTo(decodeURIComponent(attributes.href))) {
|
||||
const mention = mentionRender(decodeURIComponent(attributes.href));
|
||||
if (mention) return mention;
|
||||
}
|
||||
|
||||
return <a {...attributes}>{content}</a>;
|
||||
};
|
||||
return render;
|
||||
};
|
||||
|
||||
export const scaleSystemEmoji = (text: string): (string | JSX.Element)[] =>
|
||||
findAndReplace(
|
||||
text,
|
||||
|
|
@ -76,8 +185,9 @@ export const highlightText = (
|
|||
|
||||
export const getReactCustomHtmlParser = (
|
||||
mx: MatrixClient,
|
||||
room: Room,
|
||||
roomId: string | undefined,
|
||||
params: {
|
||||
linkifyOpts: LinkifyOpts;
|
||||
highlightRegex?: RegExp;
|
||||
handleSpoilerClick?: ReactEventHandler<HTMLElement>;
|
||||
handleMentionClick?: ReactEventHandler<HTMLElement>;
|
||||
|
|
@ -215,54 +325,14 @@ export const getReactCustomHtmlParser = (
|
|||
}
|
||||
}
|
||||
|
||||
if (name === 'a') {
|
||||
const mention = decodeURIComponent(props.href).match(
|
||||
/^https?:\/\/matrix.to\/#\/((@|#|!).+:[^?/]+)/
|
||||
if (name === 'a' && testMatrixTo(decodeURIComponent(props.href))) {
|
||||
const mention = renderMatrixMention(
|
||||
mx,
|
||||
roomId,
|
||||
decodeURIComponent(props.href),
|
||||
makeMentionCustomProps(params.handleMentionClick)
|
||||
);
|
||||
if (mention) {
|
||||
// convert mention link to pill
|
||||
const mentionId = mention[1];
|
||||
const mentionPrefix = mention[2];
|
||||
if (mentionPrefix === '#' || mentionPrefix === '!') {
|
||||
const mentionRoom = mx.getRoom(
|
||||
mentionPrefix === '#' ? getCanonicalAliasRoomId(mx, mentionId) : mentionId
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
{...props}
|
||||
className={css.Mention({
|
||||
highlight: room.roomId === (mentionRoom?.roomId ?? mentionId),
|
||||
})}
|
||||
data-mention-id={mentionRoom?.roomId ?? mentionId}
|
||||
data-mention-href={props.href}
|
||||
role="button"
|
||||
tabIndex={params.handleMentionClick ? 0 : -1}
|
||||
onKeyDown={params.handleMentionClick}
|
||||
onClick={params.handleMentionClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{domToReact(children, opts)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (mentionPrefix === '@')
|
||||
return (
|
||||
<span
|
||||
{...props}
|
||||
className={css.Mention({ highlight: mx.getUserId() === mentionId })}
|
||||
data-mention-id={mentionId}
|
||||
data-mention-href={props.href}
|
||||
role="button"
|
||||
tabIndex={params.handleMentionClick ? 0 : -1}
|
||||
onKeyDown={params.handleMentionClick}
|
||||
onClick={params.handleMentionClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{`@${getMemberDisplayName(room, mentionId) ?? getMxIdLocalPart(mentionId)}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (mention) return mention;
|
||||
}
|
||||
|
||||
if (name === 'span' && 'data-mx-spoiler' in props) {
|
||||
|
|
@ -316,7 +386,7 @@ export const getReactCustomHtmlParser = (
|
|||
}
|
||||
|
||||
if (linkify) {
|
||||
return <Linkify options={LINKIFY_OPTS}>{jsx}</Linkify>;
|
||||
return <Linkify options={params.linkifyOpts}>{jsx}</Linkify>;
|
||||
}
|
||||
return jsx;
|
||||
}
|
||||
|
|
|
|||
65
src/app/plugins/via-servers.ts
Normal file
65
src/app/plugins/via-servers.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { Room } from 'matrix-js-sdk';
|
||||
import { IPowerLevels } from '../hooks/usePowerLevels';
|
||||
import { getMxIdServer } from '../utils/matrix';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { getStateEvent } from '../utils/room';
|
||||
|
||||
export const getViaServers = (room: Room): string[] => {
|
||||
const getHighestPowerUserId = (): string | undefined => {
|
||||
const powerLevels = getStateEvent(room, StateEvent.RoomPowerLevels)?.getContent<IPowerLevels>();
|
||||
|
||||
if (!powerLevels) return undefined;
|
||||
const userIdToPower = powerLevels.users;
|
||||
if (!userIdToPower) return undefined;
|
||||
let powerUserId: string | undefined;
|
||||
|
||||
Object.keys(userIdToPower).forEach((userId) => {
|
||||
if (userIdToPower[userId] <= (powerLevels.users_default ?? 0)) return;
|
||||
|
||||
if (!powerUserId) {
|
||||
powerUserId = userId;
|
||||
return;
|
||||
}
|
||||
if (userIdToPower[userId] > userIdToPower[powerUserId]) {
|
||||
powerUserId = userId;
|
||||
}
|
||||
});
|
||||
return powerUserId;
|
||||
};
|
||||
|
||||
const getServerToPopulation = (): Record<string, number> => {
|
||||
const members = room.getMembers();
|
||||
const serverToPop: Record<string, number> = {};
|
||||
|
||||
members?.forEach((member) => {
|
||||
const { userId } = member;
|
||||
const server = getMxIdServer(userId);
|
||||
if (!server) return;
|
||||
const serverPop = serverToPop[server];
|
||||
if (serverPop === undefined) {
|
||||
serverToPop[server] = 1;
|
||||
return;
|
||||
}
|
||||
serverToPop[server] = serverPop + 1;
|
||||
});
|
||||
|
||||
return serverToPop;
|
||||
};
|
||||
|
||||
const via: string[] = [];
|
||||
const userId = getHighestPowerUserId();
|
||||
if (userId) {
|
||||
const server = getMxIdServer(userId);
|
||||
if (server) via.push(server);
|
||||
}
|
||||
const serverToPop = getServerToPopulation();
|
||||
const sortedServers = Object.keys(serverToPop).sort(
|
||||
(svrA, svrB) => serverToPop[svrB] - serverToPop[svrA]
|
||||
);
|
||||
const mostPop3 = sortedServers.slice(0, 3);
|
||||
if (via.length === 0) return mostPop3;
|
||||
if (mostPop3.includes(via[0])) {
|
||||
mostPop3.splice(mostPop3.indexOf(via[0]), 1);
|
||||
}
|
||||
return via.concat(mostPop3.slice(0, 2));
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue