render reply in single line and render custom html
This commit is contained in:
parent
682d5be11d
commit
95bfa25b58
17 changed files with 632 additions and 228 deletions
136
src/app/components/RenderReplyContent.tsx
Normal file
136
src/app/components/RenderReplyContent.tsx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import React from 'react';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import { Opts } from 'linkifyjs';
|
||||
import { Text } from 'folds';
|
||||
import {
|
||||
MessageAudioContent,
|
||||
MessageFileContent,
|
||||
MessageImageContent,
|
||||
MessageStickerContent,
|
||||
MessageVideoContent,
|
||||
UnsupportedContent,
|
||||
ReplyMEmote,
|
||||
ReplyMNotice,
|
||||
ReplyMText,
|
||||
ReplyMLocation,
|
||||
} from './message';
|
||||
import { MsgType } from '../../types/matrix/common';
|
||||
import { rContent } from '../../client/reader';
|
||||
import { MessageEvent } from '../../types/matrix/room';
|
||||
|
||||
type RenderReplyContentProps = {
|
||||
displayName: string;
|
||||
eventType: string;
|
||||
edited?: boolean;
|
||||
content: object;
|
||||
highlightRegex?: RegExp;
|
||||
htmlReactParserOptions: HTMLReactParserOptions;
|
||||
linkifyOpts: Opts;
|
||||
};
|
||||
|
||||
export function RenderReplyContent({
|
||||
displayName,
|
||||
eventType,
|
||||
edited,
|
||||
content,
|
||||
highlightRegex,
|
||||
htmlReactParserOptions,
|
||||
linkifyOpts,
|
||||
}: RenderReplyContentProps) {
|
||||
if (eventType === MessageEvent.Sticker) {
|
||||
return (
|
||||
<Text size="T300" priority="300">
|
||||
<MessageStickerContent />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType !== MessageEvent.RoomMessage) {
|
||||
return <UnsupportedContent size="T300" />;
|
||||
}
|
||||
|
||||
const msgType = rContent.msgtype(content);
|
||||
|
||||
if (msgType === MsgType.Text) {
|
||||
return (
|
||||
<ReplyMText
|
||||
edited={edited}
|
||||
content={content}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (msgType === MsgType.Emote) {
|
||||
return (
|
||||
<ReplyMEmote
|
||||
displayName={displayName}
|
||||
edited={edited}
|
||||
content={content}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (msgType === MsgType.Notice) {
|
||||
return (
|
||||
<ReplyMNotice
|
||||
edited={edited}
|
||||
content={content}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (msgType === MsgType.Image) {
|
||||
return (
|
||||
<Text size="T300" priority="300">
|
||||
<MessageImageContent />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (msgType === MsgType.Video) {
|
||||
return (
|
||||
<Text size="T300" priority="300">
|
||||
<MessageVideoContent />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (msgType === MsgType.Audio) {
|
||||
return (
|
||||
<Text size="T300" priority="300">
|
||||
<MessageAudioContent />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (msgType === MsgType.File) {
|
||||
return (
|
||||
<Text size="T300" priority="300">
|
||||
<MessageFileContent />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (msgType === MsgType.Location) {
|
||||
return <ReplyMLocation content={content} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ReplyMText
|
||||
edited={edited}
|
||||
content={content}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -25,36 +25,41 @@ import { rContent, rEncryptedFile, rInfo } from '../../../client/reader';
|
|||
import { UrlPreviewCard, UrlPreviewHolder } from '../url-preview';
|
||||
import { testMatrixTo } from '../../plugins/matrix-to';
|
||||
|
||||
export function MBadEncrypted() {
|
||||
type TextProps = {
|
||||
size?: 'T400' | 'T300';
|
||||
};
|
||||
|
||||
export function MBadEncrypted({ size }: TextProps) {
|
||||
return (
|
||||
<Text>
|
||||
<Text size={size}>
|
||||
<MessageBadEncryptedContent />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
type RedactedContentProps = {
|
||||
type RedactedContentProps = TextProps & {
|
||||
reason?: string;
|
||||
};
|
||||
export function RedactedContent({ reason }: RedactedContentProps) {
|
||||
|
||||
export function RedactedContent({ size, reason }: RedactedContentProps) {
|
||||
return (
|
||||
<Text>
|
||||
<Text size={size}>
|
||||
<MessageDeletedContent reason={reason} />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export function UnsupportedContent() {
|
||||
export function UnsupportedContent({ size }: TextProps) {
|
||||
return (
|
||||
<Text>
|
||||
<Text size={size}>
|
||||
<MessageUnsupportedContent />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export function BrokenContent() {
|
||||
export function BrokenContent({ size }: TextProps) {
|
||||
return (
|
||||
<Text>
|
||||
<Text size={size}>
|
||||
<MessageBrokenContent />
|
||||
</Text>
|
||||
);
|
||||
|
|
|
|||
130
src/app/components/message/MsgTypeReplyRenderers.tsx
Normal file
130
src/app/components/message/MsgTypeReplyRenderers.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import React, { CSSProperties } from 'react';
|
||||
import { Text } from 'folds';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import { Opts } from 'linkifyjs';
|
||||
import { trimReplyFromBody } from '../../utils/room';
|
||||
import { MessageTextBody } from './layout';
|
||||
import { MessageEditedContent } from './content';
|
||||
import { RenderBody } from './RenderBody';
|
||||
import { rContent } from '../../../client/reader';
|
||||
import { parseGeoUri } from '../../utils/common';
|
||||
import { BrokenContent } from './MsgTypeRenderers';
|
||||
|
||||
type ReplyMTextProps = {
|
||||
edited?: boolean;
|
||||
content: object;
|
||||
style?: CSSProperties;
|
||||
highlightRegex?: RegExp;
|
||||
htmlReactParserOptions: HTMLReactParserOptions;
|
||||
linkifyOpts: Opts;
|
||||
};
|
||||
export function ReplyMText({
|
||||
edited,
|
||||
content,
|
||||
style,
|
||||
highlightRegex,
|
||||
linkifyOpts,
|
||||
htmlReactParserOptions,
|
||||
}: ReplyMTextProps) {
|
||||
const body = rContent.body(content);
|
||||
const customBody = rContent.formattedBody(content);
|
||||
const trimmedBody = trimReplyFromBody(body ?? '');
|
||||
|
||||
if (typeof body !== 'string') return <BrokenContent />;
|
||||
|
||||
return (
|
||||
<MessageTextBody size="T300" style={style} truncate>
|
||||
<RenderBody
|
||||
body={trimmedBody}
|
||||
customBody={customBody}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
{edited && <MessageEditedContent />}
|
||||
</MessageTextBody>
|
||||
);
|
||||
}
|
||||
|
||||
type ReplyMEmoteProps = {
|
||||
displayName: string;
|
||||
edited?: boolean;
|
||||
content: object;
|
||||
highlightRegex?: RegExp;
|
||||
htmlReactParserOptions: HTMLReactParserOptions;
|
||||
linkifyOpts: Opts;
|
||||
};
|
||||
export function ReplyMEmote({
|
||||
displayName,
|
||||
edited,
|
||||
content,
|
||||
highlightRegex,
|
||||
linkifyOpts,
|
||||
htmlReactParserOptions,
|
||||
}: ReplyMEmoteProps) {
|
||||
const body = rContent.body(content);
|
||||
const customBody = rContent.formattedBody(content);
|
||||
|
||||
if (typeof body !== 'string') return <BrokenContent />;
|
||||
const trimmedBody = trimReplyFromBody(body);
|
||||
|
||||
return (
|
||||
<MessageTextBody size="T300" emote preWrap={typeof customBody !== 'string'} truncate>
|
||||
<b>{`${displayName} `}</b>
|
||||
<RenderBody
|
||||
body={trimmedBody}
|
||||
customBody={customBody}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
{edited && <MessageEditedContent />}
|
||||
</MessageTextBody>
|
||||
);
|
||||
}
|
||||
|
||||
type ReplyMNoticeProps = {
|
||||
edited?: boolean;
|
||||
content: object;
|
||||
highlightRegex?: RegExp;
|
||||
htmlReactParserOptions: HTMLReactParserOptions;
|
||||
linkifyOpts: Opts;
|
||||
};
|
||||
export function ReplyMNotice({
|
||||
edited,
|
||||
content,
|
||||
highlightRegex,
|
||||
linkifyOpts,
|
||||
htmlReactParserOptions,
|
||||
}: ReplyMNoticeProps) {
|
||||
const body = rContent.body(content);
|
||||
const customBody = rContent.formattedBody(content);
|
||||
|
||||
if (typeof body !== 'string') return <BrokenContent />;
|
||||
const trimmedBody = trimReplyFromBody(body);
|
||||
|
||||
return (
|
||||
<MessageTextBody size="T300" notice preWrap={typeof customBody !== 'string'} truncate>
|
||||
<RenderBody
|
||||
body={trimmedBody}
|
||||
customBody={customBody}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
{edited && <MessageEditedContent />}
|
||||
</MessageTextBody>
|
||||
);
|
||||
}
|
||||
|
||||
type ReplyMLocationProps = {
|
||||
content: object;
|
||||
};
|
||||
export function ReplyMLocation({ content }: ReplyMLocationProps) {
|
||||
const geoUri = rContent.geoUri(content);
|
||||
if (!geoUri) return <BrokenContent />;
|
||||
const location = parseGeoUri(geoUri);
|
||||
if (!location) return <BrokenContent />;
|
||||
|
||||
return <Text size="T300">{geoUri}</Text>;
|
||||
}
|
||||
|
|
@ -1,26 +1,11 @@
|
|||
import React from 'react';
|
||||
import parse, { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import Linkify from 'linkify-react';
|
||||
import { Chip, Text } from 'folds';
|
||||
import { Opts } from 'linkifyjs';
|
||||
import { MessageEmptyContent } from './content';
|
||||
import { sanitizeCustomHtml } from '../../utils/sanitize';
|
||||
import { highlightText, scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
|
||||
|
||||
export function shouldTruncate(
|
||||
body: string,
|
||||
customBody?: string,
|
||||
maxLines = 15,
|
||||
charPerLine = 100
|
||||
) {
|
||||
// Count text characters (strip HTML tags)
|
||||
const text = customBody?.replace(/<[^>]+>/g, '') || body;
|
||||
|
||||
const newlines = (body.match(/\n/g) || []).length;
|
||||
|
||||
return text.length > maxLines * charPerLine || newlines > maxLines;
|
||||
}
|
||||
|
||||
const NEXT_LINE_ONLY_REGEX = /^\n+$/;
|
||||
|
||||
type RenderBodyProps = {
|
||||
|
|
@ -50,30 +35,3 @@ export function RenderBody({
|
|||
</Linkify>
|
||||
);
|
||||
}
|
||||
|
||||
export function ViewMoreLessButton({
|
||||
collapsed,
|
||||
onToggle,
|
||||
}: {
|
||||
collapsed: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Chip
|
||||
type="button"
|
||||
variant="Primary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
onClick={onToggle}
|
||||
outlined
|
||||
style={{
|
||||
position: collapsed ? 'sticky' : 'initial',
|
||||
bottom: '0',
|
||||
borderBottomLeftRadius: collapsed ? 0 : undefined,
|
||||
borderBottomRightRadius: collapsed ? 0 : undefined,
|
||||
}}
|
||||
>
|
||||
<Text size="L400">{collapsed ? 'View More' : 'View Less'}</Text>
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export const ReplyBar = style({
|
|||
marginLeft: 'auto',
|
||||
|
||||
width: toRem(19),
|
||||
height: `calc(100% - ${toRem(8)})`,
|
||||
height: `calc(100% - ${toRem(4)})`,
|
||||
|
||||
borderLeft: `${config.borderWidth.B500} solid ${color.Surface.ContainerLine}`,
|
||||
borderTop: `${config.borderWidth.B500} solid ${color.Surface.ContainerLine}`,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, Text, color, toRem } from 'folds';
|
||||
import { Box, Text, color, config, toRem } from 'folds';
|
||||
import React, { MouseEventHandler, ReactNode, useCallback, useMemo } from 'react';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import { Opts } from 'linkifyjs';
|
||||
|
|
@ -12,30 +12,60 @@ import { GetMemberPowerTag } from '../../hooks/useMemberPowerTag';
|
|||
import { GetRoomMemberNameCallback } from '../../hooks/useRoomMembers';
|
||||
import { rUnsigned } from '../../../client/reader';
|
||||
import { TimelineView } from '../../../client/room';
|
||||
import { RenderMessageContent } from '../RenderMessageContent';
|
||||
import { RenderReplyContent } from '../RenderReplyContent';
|
||||
import { Time } from './Time';
|
||||
import { BubbleLayout, CompactLayout, MessageBase, ModernLayout } from './layout';
|
||||
import { MessageLayout, MessageSpacing } from '../../state/settings';
|
||||
import { RedactedContent } from './MsgTypeRenderers';
|
||||
import { HoverUnderline } from '../../styles/Text.css';
|
||||
|
||||
type ReplyMessageProps = {
|
||||
username?: ReactNode;
|
||||
time?: ReactNode;
|
||||
messageLayout: MessageLayout;
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
};
|
||||
export function ReplyMessage({ username, time, messageLayout, children }: ReplyMessageProps) {
|
||||
export function ReplyMessage({
|
||||
username,
|
||||
time,
|
||||
messageLayout,
|
||||
onClick,
|
||||
children,
|
||||
}: ReplyMessageProps) {
|
||||
const msgBarJSX = <div className={css.ReplyBar} />;
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!onClick) return;
|
||||
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
const commonProps = onClick
|
||||
? {
|
||||
role: 'button' as const,
|
||||
tabIndex: 0,
|
||||
onClick,
|
||||
onKeyDown: handleKeyDown,
|
||||
className: HoverUnderline,
|
||||
style: { cursor: 'pointer' },
|
||||
}
|
||||
: {};
|
||||
|
||||
if (messageLayout === MessageLayout.Compact) {
|
||||
return (
|
||||
<CompactLayout before={msgBarJSX}>
|
||||
<CompactLayout before={msgBarJSX} {...commonProps}>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Box alignItems="Baseline" shrink="No" gap="200">
|
||||
{username}
|
||||
<Box alignItems="Center" shrink="No" gap="200">
|
||||
<Box shrink="No" style={{ maxWidth: toRem(200) }}>
|
||||
{username}
|
||||
</Box>
|
||||
{children}
|
||||
{time}
|
||||
</Box>
|
||||
{children}
|
||||
</Box>
|
||||
</CompactLayout>
|
||||
);
|
||||
|
|
@ -43,28 +73,27 @@ export function ReplyMessage({ username, time, messageLayout, children }: ReplyM
|
|||
|
||||
if (messageLayout === MessageLayout.Bubble) {
|
||||
return (
|
||||
<BubbleLayout
|
||||
hideBubble
|
||||
before={msgBarJSX}
|
||||
header={
|
||||
<Box alignItems="Baseline" shrink="No" gap="200">
|
||||
<BubbleLayout hideBubble before={msgBarJSX} {...commonProps}>
|
||||
<Box alignItems="Center" shrink="No" gap="200">
|
||||
<Box shrink="No" style={{ maxWidth: toRem(200) }}>
|
||||
{username}
|
||||
{time}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
{children}
|
||||
{time}
|
||||
</Box>
|
||||
</BubbleLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ModernLayout before={msgBarJSX}>
|
||||
<Box alignItems="Baseline" shrink="No" gap="200">
|
||||
{username}
|
||||
<ModernLayout before={msgBarJSX} {...commonProps}>
|
||||
<Box alignItems="Center" shrink="No" gap="200">
|
||||
<Box shrink="No" style={{ maxWidth: toRem(200) }}>
|
||||
{username}
|
||||
</Box>
|
||||
{children}
|
||||
{time}
|
||||
</Box>
|
||||
{children}
|
||||
</ModernLayout>
|
||||
);
|
||||
}
|
||||
|
|
@ -72,7 +101,7 @@ export function ReplyMessage({ username, time, messageLayout, children }: ReplyM
|
|||
type ReplyProps = {
|
||||
roomId: string;
|
||||
eventId: string;
|
||||
onClick?: MouseEventHandler | undefined;
|
||||
onClick?: (replyId: string) => void;
|
||||
getMemberPowerTag?: GetMemberPowerTag;
|
||||
accessibleTagColors?: Map<string, string>;
|
||||
legacyUsernameColor?: boolean;
|
||||
|
|
@ -121,9 +150,11 @@ export function Reply({
|
|||
|
||||
const redactedEvent = redacted || !!rUnsigned.redactedBecause(replyEvent?.unsigned);
|
||||
|
||||
// TODO: make edit message appear in reply
|
||||
return (
|
||||
<MessageBase space={messageSpacing}>
|
||||
<ReplyMessage
|
||||
onClick={onClick ? () => onClick(eventId) : undefined}
|
||||
messageLayout={messageLayout}
|
||||
username={
|
||||
sender && (
|
||||
|
|
@ -145,20 +176,18 @@ export function Reply({
|
|||
>
|
||||
{replyEvent !== undefined ? (
|
||||
<Box direction="Column" alignSelf="Start" style={{ maxWidth: '100%' }} as="div">
|
||||
{redactedEvent && <RedactedContent />}
|
||||
{redactedEvent && <RedactedContent size="T300" />}
|
||||
{!redactedEvent &&
|
||||
(replyEvent === null ? (
|
||||
<Text>
|
||||
<Text size="B300">
|
||||
<MessageBadEncryptedContent />
|
||||
</Text>
|
||||
) : (
|
||||
<RenderMessageContent
|
||||
<RenderReplyContent
|
||||
displayName={getMemberName(replyEvent.sender)}
|
||||
eventType={replyEvent.type}
|
||||
ts={replyEvent.origin_server_ts}
|
||||
edited={false}
|
||||
edited={false} // TODO:
|
||||
content={replyEvent.content}
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
|
|
@ -170,6 +199,7 @@ export function Reply({
|
|||
backgroundColor: color.SurfaceVariant.ContainerActive,
|
||||
width: toRem(placeholderWidth),
|
||||
maxWidth: '100%',
|
||||
height: config.lineHeight.T300,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -64,3 +64,38 @@ export const MessageEditedContent = as<'span', { children?: never }>(({ ...props
|
|||
{' (edited)'}
|
||||
</Text>
|
||||
));
|
||||
|
||||
export const MessageStickerContent = as<'div', { children?: never }>(({ ...props }, ref) => (
|
||||
<Box as="span" alignItems="Center" gap="100" {...props} ref={ref}>
|
||||
<Icon size="50" src={Icons.Sticker} filled />
|
||||
<i>Sticker</i>
|
||||
</Box>
|
||||
));
|
||||
|
||||
export const MessageImageContent = as<'div', { children?: never }>(({ ...props }, ref) => (
|
||||
<Box as="span" alignItems="Center" gap="100" {...props} ref={ref}>
|
||||
<Icon size="50" src={Icons.Photo} filled />
|
||||
<i>Image</i>
|
||||
</Box>
|
||||
));
|
||||
|
||||
export const MessageVideoContent = as<'div', { children?: never }>(({ ...props }, ref) => (
|
||||
<Box as="span" alignItems="Center" gap="100" {...props} ref={ref}>
|
||||
<Icon size="50" src={Icons.Vlc} filled />
|
||||
<i>Video</i>
|
||||
</Box>
|
||||
));
|
||||
|
||||
export const MessageAudioContent = as<'div', { children?: never }>(({ ...props }, ref) => (
|
||||
<Box as="span" alignItems="Center" gap="100" {...props} ref={ref}>
|
||||
<Icon size="50" src={Icons.Play} filled />
|
||||
<i>Audio</i>
|
||||
</Box>
|
||||
));
|
||||
|
||||
export const MessageFileContent = as<'div', { children?: never }>(({ ...props }, ref) => (
|
||||
<Box as="span" alignItems="Center" gap="100" {...props} ref={ref}>
|
||||
<Icon size="50" src={Icons.File} filled />
|
||||
<i>File</i>
|
||||
</Box>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@ export * from './Reply';
|
|||
export * from './content';
|
||||
export * from './Time';
|
||||
export * from './MsgTypeRenderers';
|
||||
export * from './MsgTypeReplyRenderers';
|
||||
export * from './FileHeader';
|
||||
export * from './RenderBody';
|
||||
|
|
|
|||
|
|
@ -28,19 +28,20 @@ export const UsernameBold = as<'b'>(({ as: AsUsernameBold = 'b', className, ...p
|
|||
<AsUsernameBold className={classNames(css.UsernameBold, className)} {...props} ref={ref} />
|
||||
));
|
||||
|
||||
export const MessageTextBody = as<'div', css.MessageTextBodyVariants & { notice?: boolean }>(
|
||||
export const MessageTextBody = as<
|
||||
'div',
|
||||
css.MessageTextBodyVariants & { size?: 'T400' | 'T300'; notice?: boolean; truncate?: boolean }
|
||||
>(
|
||||
(
|
||||
{ as: asComp = 'div', className, preWrap, jumboEmoji, emote, notice, truncate, ...props },
|
||||
{ as: asComp = 'div', className, size, preWrap, jumboEmoji, emote, notice, truncate, ...props },
|
||||
ref
|
||||
) => (
|
||||
<Text
|
||||
as={asComp}
|
||||
size="T400"
|
||||
size={size ?? 'T400'}
|
||||
priority={notice ? '300' : '400'}
|
||||
className={classNames(
|
||||
css.MessageTextBody({ preWrap, jumboEmoji, emote, truncate }),
|
||||
className
|
||||
)}
|
||||
className={classNames(css.MessageTextBody({ preWrap, jumboEmoji, emote }), className)}
|
||||
truncate={truncate}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -201,27 +201,6 @@ export const MessageTextBody = recipe({
|
|||
fontStyle: 'italic',
|
||||
},
|
||||
},
|
||||
truncate: {
|
||||
true: {
|
||||
maxHeight: '15em',
|
||||
overflow: 'hidden',
|
||||
|
||||
selectors: {
|
||||
'&::before': {
|
||||
content: '',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
pointerEvents: 'none',
|
||||
|
||||
height: config.space.S400,
|
||||
background: `linear-gradient(to top, ${color.Primary.Container}, #00000000)`,
|
||||
borderBottom: `${config.borderWidth.B400} solid ${color.Primary.ContainerLine}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -55,10 +55,14 @@ export function Room() {
|
|||
useCallback(
|
||||
(evt) => {
|
||||
if (isKeyHotkey('escape', evt)) {
|
||||
// TODO: check overlay's
|
||||
if (activeThreadId && threadFocusRef.current) {
|
||||
timeline?.closeThread();
|
||||
}
|
||||
// markAsRead(mx, roomId, hideActivity);
|
||||
}
|
||||
},
|
||||
[roomId, hideActivity]
|
||||
[activeThreadId, timeline]
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,9 @@ import React, {
|
|||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { Editor } from 'slate';
|
||||
import { Box, Scroll, config, toRem } from 'folds';
|
||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||
import { MessageEventWithoutRoomId } from 'neurone/lib/api/types';
|
||||
import { useVirtualPaginator } from '../../hooks/useVirtualPaginator';
|
||||
import { useAlive } from '../../hooks/useAlive';
|
||||
|
|
@ -24,13 +22,6 @@ import {
|
|||
Reply,
|
||||
RedactedContent,
|
||||
} from '../../components/message';
|
||||
import {
|
||||
factoryRenderLinkifyWithMention,
|
||||
getReactCustomHtmlParser,
|
||||
LINKIFY_OPTS,
|
||||
makeMentionCustomProps,
|
||||
renderMatrixMention,
|
||||
} from '../../plugins/react-custom-html-parser';
|
||||
import { getLatestEditEvent, getReplyId, getThreadId } from '../../utils/room';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { MessageLayout, settingsAtom } from '../../state/settings';
|
||||
|
|
@ -39,10 +30,7 @@ import { RoomIntro } from '../../components/room-intro';
|
|||
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||
import { MessageEvent, StateEvent } from '../../../types/matrix/room';
|
||||
import { RenderMessageContent } from '../../components/RenderMessageContent';
|
||||
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
|
||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
|
||||
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
|
||||
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
|
||||
|
|
@ -79,6 +67,7 @@ import { TimelineFloat, JumpToLatestButton, ComboDivider, PlaceholderList } from
|
|||
import { OutboxView } from './OutboxView';
|
||||
import { EncryptedMessageEvent } from '../../components/encrypted-event';
|
||||
import { useUserReadReceipt } from '../../hooks/useReadReceipt';
|
||||
import { useMessageParserOptions } from '../../hooks/useMessageParserOptions';
|
||||
|
||||
type RoomTimelineProps = {
|
||||
eventId?: string;
|
||||
|
|
@ -92,7 +81,6 @@ const PAGINATION_LIMIT = 80;
|
|||
export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTimelineProps) {
|
||||
const ct = useClient();
|
||||
const roomId = useRoom();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const encryptedRooms = useObservableValue(ct.encryptedRooms);
|
||||
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
|
|
@ -143,8 +131,6 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
|
|||
// const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
// const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
const mentionClickHandler = useMentionClickHandler(roomId);
|
||||
const spoilerClickHandler = useSpoilerClickHandler();
|
||||
|
||||
const imagePackRooms = useImagePackRooms(roomId);
|
||||
|
||||
|
|
@ -166,43 +152,8 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
|
|||
>();
|
||||
const alive = useAlive();
|
||||
|
||||
const linkifyOpts = useMemo<LinkifyOpts>(
|
||||
() => ({
|
||||
...LINKIFY_OPTS,
|
||||
render: factoryRenderLinkifyWithMention((href) =>
|
||||
renderMatrixMention(
|
||||
ct,
|
||||
roomId,
|
||||
href,
|
||||
makeMentionCustomProps(mentionClickHandler),
|
||||
membersAPI.getName,
|
||||
getRoomName
|
||||
)
|
||||
),
|
||||
}),
|
||||
[ct, roomId, mentionClickHandler, membersAPI.getName, getRoomName]
|
||||
);
|
||||
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
||||
() =>
|
||||
getReactCustomHtmlParser(ct, roomId, {
|
||||
linkifyOpts,
|
||||
useAuthentication,
|
||||
handleSpoilerClick: spoilerClickHandler,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
getMemberName: membersAPI.getName,
|
||||
getRoomName,
|
||||
}),
|
||||
[
|
||||
ct,
|
||||
roomId,
|
||||
linkifyOpts,
|
||||
spoilerClickHandler,
|
||||
mentionClickHandler,
|
||||
useAuthentication,
|
||||
membersAPI.getName,
|
||||
getRoomName,
|
||||
]
|
||||
);
|
||||
const { linkifyOpts, htmlReactParserOptions, htmlReactParserInlineOptions } =
|
||||
useMessageParserOptions(roomId, getRoomName, membersAPI.getName);
|
||||
|
||||
const activeThread = useObservableValue(timeline.activeThread);
|
||||
const threads = useObservableValue(timeline.threads);
|
||||
|
|
@ -315,10 +266,8 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
|
|||
useObservableValueListener(timeline.outbox.senders, stickToBottom);
|
||||
useObservableValueListener(timeline.range.info, stickToBottom);
|
||||
|
||||
const handleOpenReply: MouseEventHandler = useCallback(async (evt) => {
|
||||
const targetId = evt.currentTarget.getAttribute('data-event-id');
|
||||
if (!targetId) return;
|
||||
console.log(targetId);
|
||||
const handleOpenReply = useCallback((replyId: string) => {
|
||||
console.log(replyId);
|
||||
}, []);
|
||||
|
||||
const handleUserClick = useUserClickHandler();
|
||||
|
|
@ -553,7 +502,7 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
|
|||
redacted={redacted.has(replyEventId)}
|
||||
getMemberName={membersAPI.getName}
|
||||
timeline={timeline}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
htmlReactParserOptions={htmlReactParserInlineOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { DefaultReset, color, config, toRem } from 'folds';
|
|||
|
||||
export const TimelineDivider = style({
|
||||
position: 'relative',
|
||||
minHeight: toRem(20),
|
||||
});
|
||||
export const TimelineDividerCenterChild = style({
|
||||
position: 'absolute',
|
||||
|
|
|
|||
|
|
@ -7,10 +7,8 @@ import React, {
|
|||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { Box, Scroll, Text, config } from 'folds';
|
||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||
import { MessageEventWithoutRoomId } from 'neurone/lib/api/types';
|
||||
import { useVirtualPaginator } from '../../../hooks/useVirtualPaginator';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
|
|
@ -21,13 +19,6 @@ import {
|
|||
Reply,
|
||||
DividerTime,
|
||||
} from '../../../components/message';
|
||||
import {
|
||||
factoryRenderLinkifyWithMention,
|
||||
getReactCustomHtmlParser,
|
||||
LINKIFY_OPTS,
|
||||
makeMentionCustomProps,
|
||||
renderMatrixMention,
|
||||
} from '../../../plugins/react-custom-html-parser';
|
||||
import { fallbackReply, getLatestEditEvent, getReplyId } from '../../../utils/room';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { MessageLayout, settingsAtom } from '../../../state/settings';
|
||||
|
|
@ -36,9 +27,6 @@ import * as css from './styles.css';
|
|||
import { usePowerLevelsContext } from '../../../hooks/usePowerLevels';
|
||||
import { MessageEvent, StateEvent } from '../../../../types/matrix/room';
|
||||
import { RenderMessageContent } from '../../../components/RenderMessageContent';
|
||||
import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
|
||||
import { useImagePackRooms } from '../../../hooks/useImagePackRooms';
|
||||
import { useIsDirectRoom, useRoom } from '../../../hooks/useRoom';
|
||||
|
|
@ -75,6 +63,7 @@ import { useEditor } from '../../../components/editor';
|
|||
import { TimelineFloat, JumpToLatestButton, ComboDivider, PlaceholderList } from '../components';
|
||||
import { OutboxView } from '../OutboxView';
|
||||
import { useUserReadReceipt } from '../../../hooks/useReadReceipt';
|
||||
import { useMessageParserOptions } from '../../../hooks/useMessageParserOptions';
|
||||
|
||||
type TimelineViewProps = {
|
||||
threadTimeline: ThreadTimeline;
|
||||
|
|
@ -85,7 +74,6 @@ const PAGINATION_LIMIT = 80;
|
|||
export function TimelineView({ threadTimeline }: TimelineViewProps) {
|
||||
const ct = useClient();
|
||||
const roomId = useRoom();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const encryptedRooms = useObservableValue(ct.encryptedRooms);
|
||||
const editor = useEditor();
|
||||
const roomInputRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -134,8 +122,6 @@ export function TimelineView({ threadTimeline }: TimelineViewProps) {
|
|||
const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, ct.session.userId);
|
||||
const [editId, setEditId] = useState<string>();
|
||||
|
||||
const mentionClickHandler = useMentionClickHandler(roomId);
|
||||
const spoilerClickHandler = useSpoilerClickHandler();
|
||||
const imagePackRooms = useImagePackRooms(roomId);
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -154,44 +140,8 @@ export function TimelineView({ threadTimeline }: TimelineViewProps) {
|
|||
>();
|
||||
const alive = useAlive();
|
||||
|
||||
const linkifyOpts = useMemo<LinkifyOpts>(
|
||||
() => ({
|
||||
...LINKIFY_OPTS,
|
||||
render: factoryRenderLinkifyWithMention((href) =>
|
||||
renderMatrixMention(
|
||||
ct,
|
||||
roomId,
|
||||
href,
|
||||
makeMentionCustomProps(mentionClickHandler),
|
||||
membersAPI.getName,
|
||||
getRoomName
|
||||
)
|
||||
),
|
||||
}),
|
||||
[ct, roomId, mentionClickHandler, membersAPI.getName, getRoomName]
|
||||
);
|
||||
|
||||
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
||||
() =>
|
||||
getReactCustomHtmlParser(ct, roomId, {
|
||||
linkifyOpts,
|
||||
useAuthentication,
|
||||
handleSpoilerClick: spoilerClickHandler,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
getMemberName: membersAPI.getName,
|
||||
getRoomName,
|
||||
}),
|
||||
[
|
||||
ct,
|
||||
roomId,
|
||||
linkifyOpts,
|
||||
spoilerClickHandler,
|
||||
mentionClickHandler,
|
||||
useAuthentication,
|
||||
membersAPI.getName,
|
||||
getRoomName,
|
||||
]
|
||||
);
|
||||
const { linkifyOpts, htmlReactParserOptions, htmlReactParserInlineOptions } =
|
||||
useMessageParserOptions(roomId, getRoomName, membersAPI.getName);
|
||||
|
||||
const redacted = useObservableValue(threadTimeline.relations.redacted);
|
||||
const reactions = useObservableValue(threadTimeline.relations.reactions.eventToRelations);
|
||||
|
|
@ -296,10 +246,8 @@ export function TimelineView({ threadTimeline }: TimelineViewProps) {
|
|||
}
|
||||
}, [scrollToElement, editId]);
|
||||
|
||||
const handleOpenReply: MouseEventHandler = useCallback(async (evt) => {
|
||||
const targetId = evt.currentTarget.getAttribute('data-event-id');
|
||||
if (!targetId) return;
|
||||
console.log(targetId);
|
||||
const handleOpenReply = useCallback((replyId: string) => {
|
||||
console.log(replyId);
|
||||
}, []);
|
||||
|
||||
const handleUserClick = useUserClickHandler();
|
||||
|
|
@ -450,7 +398,7 @@ export function TimelineView({ threadTimeline }: TimelineViewProps) {
|
|||
redacted={redacted.has(replyEventId)}
|
||||
getMemberName={membersAPI.getName}
|
||||
timeline={threadTimeline}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
htmlReactParserOptions={htmlReactParserInlineOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
|
|
|
|||
86
src/app/hooks/useMessageParserOptions.ts
Normal file
86
src/app/hooks/useMessageParserOptions.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import {
|
||||
factoryRenderLinkifyWithMention,
|
||||
getReactCustomHtmlInlineParser,
|
||||
getReactCustomHtmlParser,
|
||||
LINKIFY_OPTS,
|
||||
makeMentionCustomProps,
|
||||
renderMatrixMention,
|
||||
} from '../plugins/react-custom-html-parser';
|
||||
import { useClient } from './useClient';
|
||||
import { useMentionClickHandler } from './useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from './useSpoilerClickHandler';
|
||||
import { GetRoomNameCallback } from './storage/useGetRoomName';
|
||||
import { GetRoomMemberNameCallback } from './useRoomMembers';
|
||||
import { useMediaAuthentication } from './useMediaAuthentication';
|
||||
|
||||
export const useMessageParserOptions = (
|
||||
roomId: string,
|
||||
getRoomName: GetRoomNameCallback,
|
||||
getMemberName: GetRoomMemberNameCallback
|
||||
) => {
|
||||
const ct = useClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
|
||||
const mentionClickHandler = useMentionClickHandler(roomId);
|
||||
const spoilerClickHandler = useSpoilerClickHandler();
|
||||
|
||||
const linkifyOpts = useMemo<LinkifyOpts>(
|
||||
() => ({
|
||||
...LINKIFY_OPTS,
|
||||
render: factoryRenderLinkifyWithMention((href) =>
|
||||
renderMatrixMention(
|
||||
ct,
|
||||
roomId,
|
||||
href,
|
||||
makeMentionCustomProps(mentionClickHandler),
|
||||
getMemberName,
|
||||
getRoomName
|
||||
)
|
||||
),
|
||||
}),
|
||||
[ct, roomId, mentionClickHandler, getMemberName, getRoomName]
|
||||
);
|
||||
|
||||
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
||||
() =>
|
||||
getReactCustomHtmlParser(ct, roomId, {
|
||||
linkifyOpts,
|
||||
useAuthentication,
|
||||
handleSpoilerClick: spoilerClickHandler,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
getMemberName,
|
||||
getRoomName,
|
||||
}),
|
||||
[
|
||||
ct,
|
||||
roomId,
|
||||
linkifyOpts,
|
||||
spoilerClickHandler,
|
||||
mentionClickHandler,
|
||||
useAuthentication,
|
||||
getMemberName,
|
||||
getRoomName,
|
||||
]
|
||||
);
|
||||
|
||||
const htmlReactParserInlineOptions = useMemo<HTMLReactParserOptions>(
|
||||
() =>
|
||||
getReactCustomHtmlInlineParser(ct, roomId, {
|
||||
linkifyOpts,
|
||||
useAuthentication,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
getMemberName,
|
||||
getRoomName,
|
||||
}),
|
||||
[ct, roomId, linkifyOpts, useAuthentication, mentionClickHandler, getMemberName, getRoomName]
|
||||
);
|
||||
|
||||
return {
|
||||
linkifyOpts,
|
||||
htmlReactParserOptions,
|
||||
htmlReactParserInlineOptions,
|
||||
};
|
||||
};
|
||||
|
|
@ -474,14 +474,15 @@ export const getReactCustomHtmlParser = (
|
|||
}
|
||||
|
||||
if (name === 'img') {
|
||||
const htmlSrc = mxcToHttp(ct.session.baseUrl, props.src, params.useAuthentication).url();
|
||||
if (htmlSrc && props.src.startsWith('mxc://') === false) {
|
||||
if (props.src && props.src.startsWith('mxc://') === false) {
|
||||
return (
|
||||
<a href={htmlSrc} target="_blank" rel="noreferrer noopener">
|
||||
{props.alt || props.title || htmlSrc}
|
||||
<a href={props.src} target="_blank" rel="noreferrer noopener">
|
||||
{props.alt || props.title || props.src}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const htmlSrc = mxcToHttp(ct.session.baseUrl, props.src, params.useAuthentication).url();
|
||||
if (htmlSrc && 'data-mx-emoticon' in props) {
|
||||
return (
|
||||
<span className={css.EmoticonBase}>
|
||||
|
|
@ -516,3 +517,143 @@ export const getReactCustomHtmlParser = (
|
|||
};
|
||||
return opts;
|
||||
};
|
||||
|
||||
export const getReactCustomHtmlInlineParser = (
|
||||
ct: ClientContext,
|
||||
roomId: string | undefined,
|
||||
params: {
|
||||
linkifyOpts: LinkifyOpts;
|
||||
highlightRegex?: RegExp;
|
||||
useAuthentication?: boolean;
|
||||
handleMentionClick?: ReactEventHandler<HTMLElement>;
|
||||
getMemberName: GetRoomMemberNameCallback;
|
||||
getRoomName: GetRoomNameCallback;
|
||||
}
|
||||
): HTMLReactParserOptions => {
|
||||
const opts: HTMLReactParserOptions = {
|
||||
replace: (domNode) => {
|
||||
if (domNode instanceof Element && 'name' in domNode) {
|
||||
const { name, attribs, children, parent } = domNode;
|
||||
const props = attributesToProps(attribs);
|
||||
|
||||
if (
|
||||
name === 'h1' ||
|
||||
name === 'h2' ||
|
||||
name === 'h3' ||
|
||||
name === 'h4' ||
|
||||
name === 'h5' ||
|
||||
name === 'h6'
|
||||
) {
|
||||
return <strong>{domToReact(children, opts)} </strong>;
|
||||
}
|
||||
|
||||
if (name === 'br') {
|
||||
return <span> </span>;
|
||||
}
|
||||
|
||||
if (name === 'p') {
|
||||
return <span>{domToReact(children, opts)} </span>;
|
||||
}
|
||||
|
||||
if (name === 'pre') {
|
||||
return <span>{domToReact(children, opts)} </span>;
|
||||
}
|
||||
|
||||
if (name === 'blockquote') {
|
||||
return <em>{domToReact(children, opts)} </em>;
|
||||
}
|
||||
|
||||
if (name === 'ul') {
|
||||
return <span>{domToReact(children, opts)} </span>;
|
||||
}
|
||||
if (name === 'ol') {
|
||||
return <span>{domToReact(children, opts)} </span>;
|
||||
}
|
||||
|
||||
if (name === 'code') {
|
||||
if (parent && 'name' in parent && parent.name === 'pre') {
|
||||
const codeReact = domToReact(children, opts);
|
||||
if (typeof codeReact === 'string') {
|
||||
return <code className={css.CodeFont}>{codeReact}</code>;
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<Text as="code" size="T300" className={css.Code} {...props}>
|
||||
{domToReact(children, opts)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'a' && testMatrixTo(tryDecodeURIComponent(props.href))) {
|
||||
const content = children.find((child) => !(child instanceof DOMText))
|
||||
? undefined
|
||||
: children.map((c) => (c instanceof DOMText ? c.data : '')).join();
|
||||
|
||||
const mention = renderMatrixMention(
|
||||
ct,
|
||||
roomId,
|
||||
tryDecodeURIComponent(props.href),
|
||||
makeMentionCustomProps(params.handleMentionClick, content),
|
||||
params.getMemberName,
|
||||
params.getRoomName
|
||||
);
|
||||
|
||||
if (mention) return mention;
|
||||
}
|
||||
|
||||
if (name === 'span' && 'data-mx-spoiler' in props) {
|
||||
return (
|
||||
<span {...props} className={css.Spoiler()} aria-pressed style={{ cursor: 'pointer' }}>
|
||||
{domToReact(children, opts)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'img') {
|
||||
if (props.src && props.src.startsWith('mxc://') === false) {
|
||||
return (
|
||||
<a href={props.src} target="_blank" rel="noreferrer noopener">
|
||||
{props.alt || props.title || props.src}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const htmlSrc = mxcToHttp(ct.session.baseUrl, props.src, params.useAuthentication).url();
|
||||
if (htmlSrc && 'data-mx-emoticon' in props) {
|
||||
return (
|
||||
<span className={css.EmoticonBase}>
|
||||
<span className={css.Emoticon()}>
|
||||
<img {...props} className={css.EmoticonImg} src={htmlSrc} />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (htmlSrc) return <em>Click to see image</em>;
|
||||
}
|
||||
|
||||
return <span>{domToReact(children, opts)} </span>;
|
||||
}
|
||||
|
||||
if (domNode instanceof DOMText) {
|
||||
const linkify =
|
||||
!(domNode.parent && 'name' in domNode.parent && domNode.parent.name === 'code') &&
|
||||
!(domNode.parent && 'name' in domNode.parent && domNode.parent.name === 'a');
|
||||
|
||||
let jsx = scaleSystemEmoji(domNode.data);
|
||||
|
||||
if (params.highlightRegex) {
|
||||
jsx = highlightText(params.highlightRegex, jsx);
|
||||
}
|
||||
|
||||
if (linkify) {
|
||||
return <Linkify options={params.linkifyOpts}>{jsx}</Linkify>;
|
||||
}
|
||||
return jsx;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
return opts;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const BaseCode = style({
|
|||
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
borderRadius: config.radii.R300,
|
||||
});
|
||||
const CodeFont = style({
|
||||
export const CodeFont = style({
|
||||
fontFamily: 'monospace',
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue