diff --git a/src/app/components/RenderReplyContent.tsx b/src/app/components/RenderReplyContent.tsx
new file mode 100644
index 00000000..f4a85318
--- /dev/null
+++ b/src/app/components/RenderReplyContent.tsx
@@ -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 (
+
+
+
+ );
+ }
+
+ if (eventType !== MessageEvent.RoomMessage) {
+ return ;
+ }
+
+ const msgType = rContent.msgtype(content);
+
+ if (msgType === MsgType.Text) {
+ return (
+
+ );
+ }
+
+ if (msgType === MsgType.Emote) {
+ return (
+
+ );
+ }
+
+ if (msgType === MsgType.Notice) {
+ return (
+
+ );
+ }
+
+ if (msgType === MsgType.Image) {
+ return (
+
+
+
+ );
+ }
+
+ if (msgType === MsgType.Video) {
+ return (
+
+
+
+ );
+ }
+
+ if (msgType === MsgType.Audio) {
+ return (
+
+
+
+ );
+ }
+
+ if (msgType === MsgType.File) {
+ return (
+
+
+
+ );
+ }
+
+ if (msgType === MsgType.Location) {
+ return ;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx
index 1fde640d..aeb3b721 100644
--- a/src/app/components/message/MsgTypeRenderers.tsx
+++ b/src/app/components/message/MsgTypeRenderers.tsx
@@ -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 (
-
+
);
}
-type RedactedContentProps = {
+type RedactedContentProps = TextProps & {
reason?: string;
};
-export function RedactedContent({ reason }: RedactedContentProps) {
+
+export function RedactedContent({ size, reason }: RedactedContentProps) {
return (
-
+
);
}
-export function UnsupportedContent() {
+export function UnsupportedContent({ size }: TextProps) {
return (
-
+
);
}
-export function BrokenContent() {
+export function BrokenContent({ size }: TextProps) {
return (
-
+
);
diff --git a/src/app/components/message/MsgTypeReplyRenderers.tsx b/src/app/components/message/MsgTypeReplyRenderers.tsx
new file mode 100644
index 00000000..2729afbb
--- /dev/null
+++ b/src/app/components/message/MsgTypeReplyRenderers.tsx
@@ -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 ;
+
+ return (
+
+
+ {edited && }
+
+ );
+}
+
+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 ;
+ const trimmedBody = trimReplyFromBody(body);
+
+ return (
+
+ {`${displayName} `}
+
+ {edited && }
+
+ );
+}
+
+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 ;
+ const trimmedBody = trimReplyFromBody(body);
+
+ return (
+
+
+ {edited && }
+
+ );
+}
+
+type ReplyMLocationProps = {
+ content: object;
+};
+export function ReplyMLocation({ content }: ReplyMLocationProps) {
+ const geoUri = rContent.geoUri(content);
+ if (!geoUri) return ;
+ const location = parseGeoUri(geoUri);
+ if (!location) return ;
+
+ return {geoUri};
+}
diff --git a/src/app/components/message/RenderBody.tsx b/src/app/components/message/RenderBody.tsx
index d48c92a6..f9518171 100644
--- a/src/app/components/message/RenderBody.tsx
+++ b/src/app/components/message/RenderBody.tsx
@@ -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({
);
}
-
-export function ViewMoreLessButton({
- collapsed,
- onToggle,
-}: {
- collapsed: boolean;
- onToggle: () => void;
-}) {
- return (
-
- {collapsed ? 'View More' : 'View Less'}
-
- );
-}
diff --git a/src/app/components/message/Reply.css.ts b/src/app/components/message/Reply.css.ts
index 94394f88..24c11682 100644
--- a/src/app/components/message/Reply.css.ts
+++ b/src/app/components/message/Reply.css.ts
@@ -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}`,
diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx
index f3bc3c5f..32c82a33 100644
--- a/src/app/components/message/Reply.tsx
+++ b/src/app/components/message/Reply.tsx
@@ -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 = ;
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ 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 (
-
+
-
- {username}
+
+
+ {username}
+
+ {children}
{time}
- {children}
);
@@ -43,28 +73,27 @@ export function ReplyMessage({ username, time, messageLayout, children }: ReplyM
if (messageLayout === MessageLayout.Bubble) {
return (
-
+
+
+
{username}
- {time}
- }
- >
- {children}
+ {children}
+ {time}
+
);
}
return (
-
-
- {username}
+
+
+
+ {username}
+
+ {children}
{time}
- {children}
);
}
@@ -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;
legacyUsernameColor?: boolean;
@@ -121,9 +150,11 @@ export function Reply({
const redactedEvent = redacted || !!rUnsigned.redactedBecause(replyEvent?.unsigned);
+ // TODO: make edit message appear in reply
return (
onClick(eventId) : undefined}
messageLayout={messageLayout}
username={
sender && (
@@ -145,20 +176,18 @@ export function Reply({
>
{replyEvent !== undefined ? (
- {redactedEvent && }
+ {redactedEvent && }
{!redactedEvent &&
(replyEvent === null ? (
-
+
) : (
-
@@ -170,6 +199,7 @@ export function Reply({
backgroundColor: color.SurfaceVariant.ContainerActive,
width: toRem(placeholderWidth),
maxWidth: '100%',
+ height: config.lineHeight.T300,
}}
/>
)}
diff --git a/src/app/components/message/content/FallbackContent.tsx b/src/app/components/message/content/FallbackContent.tsx
index 9edb9678..8353ee89 100644
--- a/src/app/components/message/content/FallbackContent.tsx
+++ b/src/app/components/message/content/FallbackContent.tsx
@@ -64,3 +64,38 @@ export const MessageEditedContent = as<'span', { children?: never }>(({ ...props
{' (edited)'}
));
+
+export const MessageStickerContent = as<'div', { children?: never }>(({ ...props }, ref) => (
+
+
+ Sticker
+
+));
+
+export const MessageImageContent = as<'div', { children?: never }>(({ ...props }, ref) => (
+
+
+ Image
+
+));
+
+export const MessageVideoContent = as<'div', { children?: never }>(({ ...props }, ref) => (
+
+
+ Video
+
+));
+
+export const MessageAudioContent = as<'div', { children?: never }>(({ ...props }, ref) => (
+
+
+ Audio
+
+));
+
+export const MessageFileContent = as<'div', { children?: never }>(({ ...props }, ref) => (
+
+
+ File
+
+));
diff --git a/src/app/components/message/index.ts b/src/app/components/message/index.ts
index 6f7415ad..e90e2516 100644
--- a/src/app/components/message/index.ts
+++ b/src/app/components/message/index.ts
@@ -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';
diff --git a/src/app/components/message/layout/Base.tsx b/src/app/components/message/layout/Base.tsx
index b07ae257..a8957c5c 100644
--- a/src/app/components/message/layout/Base.tsx
+++ b/src/app/components/message/layout/Base.tsx
@@ -28,19 +28,20 @@ export const UsernameBold = as<'b'>(({ as: AsUsernameBold = 'b', className, ...p
));
-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
) => (
diff --git a/src/app/components/message/layout/layout.css.ts b/src/app/components/message/layout/layout.css.ts
index 0a61b361..1acd505f 100644
--- a/src/app/components/message/layout/layout.css.ts
+++ b/src/app/components/message/layout/layout.css.ts
@@ -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}`,
- },
- },
- },
- },
},
});
diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx
index b171d7eb..85301f94 100644
--- a/src/app/features/room/Room.tsx
+++ b/src/app/features/room/Room.tsx
@@ -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]
)
);
diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx
index 315638af..6ae19132 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -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(
- () => ({
- ...LINKIFY_OPTS,
- render: factoryRenderLinkifyWithMention((href) =>
- renderMatrixMention(
- ct,
- roomId,
- href,
- makeMentionCustomProps(mentionClickHandler),
- membersAPI.getName,
- getRoomName
- )
- ),
- }),
- [ct, roomId, mentionClickHandler, membersAPI.getName, getRoomName]
- );
- const htmlReactParserOptions = useMemo(
- () =>
- 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}
diff --git a/src/app/features/room/components/styles.css.ts b/src/app/features/room/components/styles.css.ts
index 440322bb..5d5cecf0 100644
--- a/src/app/features/room/components/styles.css.ts
+++ b/src/app/features/room/components/styles.css.ts
@@ -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',
diff --git a/src/app/features/room/thread-view/TimelineView.tsx b/src/app/features/room/thread-view/TimelineView.tsx
index 01275943..e85b5b8d 100644
--- a/src/app/features/room/thread-view/TimelineView.tsx
+++ b/src/app/features/room/thread-view/TimelineView.tsx
@@ -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(null);
@@ -134,8 +122,6 @@ export function TimelineView({ threadTimeline }: TimelineViewProps) {
const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, ct.session.userId);
const [editId, setEditId] = useState();
- const mentionClickHandler = useMentionClickHandler(roomId);
- const spoilerClickHandler = useSpoilerClickHandler();
const imagePackRooms = useImagePackRooms(roomId);
const scrollRef = useRef(null);
@@ -154,44 +140,8 @@ export function TimelineView({ threadTimeline }: TimelineViewProps) {
>();
const alive = useAlive();
- const linkifyOpts = useMemo(
- () => ({
- ...LINKIFY_OPTS,
- render: factoryRenderLinkifyWithMention((href) =>
- renderMatrixMention(
- ct,
- roomId,
- href,
- makeMentionCustomProps(mentionClickHandler),
- membersAPI.getName,
- getRoomName
- )
- ),
- }),
- [ct, roomId, mentionClickHandler, membersAPI.getName, getRoomName]
- );
-
- const htmlReactParserOptions = useMemo(
- () =>
- 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}
diff --git a/src/app/hooks/useMessageParserOptions.ts b/src/app/hooks/useMessageParserOptions.ts
new file mode 100644
index 00000000..6fadca79
--- /dev/null
+++ b/src/app/hooks/useMessageParserOptions.ts
@@ -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(
+ () => ({
+ ...LINKIFY_OPTS,
+ render: factoryRenderLinkifyWithMention((href) =>
+ renderMatrixMention(
+ ct,
+ roomId,
+ href,
+ makeMentionCustomProps(mentionClickHandler),
+ getMemberName,
+ getRoomName
+ )
+ ),
+ }),
+ [ct, roomId, mentionClickHandler, getMemberName, getRoomName]
+ );
+
+ const htmlReactParserOptions = useMemo(
+ () =>
+ getReactCustomHtmlParser(ct, roomId, {
+ linkifyOpts,
+ useAuthentication,
+ handleSpoilerClick: spoilerClickHandler,
+ handleMentionClick: mentionClickHandler,
+ getMemberName,
+ getRoomName,
+ }),
+ [
+ ct,
+ roomId,
+ linkifyOpts,
+ spoilerClickHandler,
+ mentionClickHandler,
+ useAuthentication,
+ getMemberName,
+ getRoomName,
+ ]
+ );
+
+ const htmlReactParserInlineOptions = useMemo(
+ () =>
+ getReactCustomHtmlInlineParser(ct, roomId, {
+ linkifyOpts,
+ useAuthentication,
+ handleMentionClick: mentionClickHandler,
+ getMemberName,
+ getRoomName,
+ }),
+ [ct, roomId, linkifyOpts, useAuthentication, mentionClickHandler, getMemberName, getRoomName]
+ );
+
+ return {
+ linkifyOpts,
+ htmlReactParserOptions,
+ htmlReactParserInlineOptions,
+ };
+};
diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx
index 9cc86d1f..6da8f251 100644
--- a/src/app/plugins/react-custom-html-parser.tsx
+++ b/src/app/plugins/react-custom-html-parser.tsx
@@ -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 (
-
- {props.alt || props.title || htmlSrc}
+
+ {props.alt || props.title || props.src}
);
}
+
+ const htmlSrc = mxcToHttp(ct.session.baseUrl, props.src, params.useAuthentication).url();
if (htmlSrc && 'data-mx-emoticon' in props) {
return (
@@ -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;
+ 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 {domToReact(children, opts)} ;
+ }
+
+ if (name === 'br') {
+ return ;
+ }
+
+ if (name === 'p') {
+ return {domToReact(children, opts)} ;
+ }
+
+ if (name === 'pre') {
+ return {domToReact(children, opts)} ;
+ }
+
+ if (name === 'blockquote') {
+ return {domToReact(children, opts)} ;
+ }
+
+ if (name === 'ul') {
+ return {domToReact(children, opts)} ;
+ }
+ if (name === 'ol') {
+ return {domToReact(children, opts)} ;
+ }
+
+ if (name === 'code') {
+ if (parent && 'name' in parent && parent.name === 'pre') {
+ const codeReact = domToReact(children, opts);
+ if (typeof codeReact === 'string') {
+ return {codeReact};
+ }
+ } else {
+ return (
+
+ {domToReact(children, opts)}
+
+ );
+ }
+ }
+
+ 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 (
+
+ {domToReact(children, opts)}
+
+ );
+ }
+
+ if (name === 'img') {
+ if (props.src && props.src.startsWith('mxc://') === false) {
+ return (
+
+ {props.alt || props.title || props.src}
+
+ );
+ }
+
+ const htmlSrc = mxcToHttp(ct.session.baseUrl, props.src, params.useAuthentication).url();
+ if (htmlSrc && 'data-mx-emoticon' in props) {
+ return (
+
+
+
+
+
+ );
+ }
+ if (htmlSrc) return Click to see image;
+ }
+
+ return {domToReact(children, opts)} ;
+ }
+
+ 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 {jsx};
+ }
+ return jsx;
+ }
+
+ return undefined;
+ },
+ };
+ return opts;
+};
diff --git a/src/app/styles/CustomHtml.css.ts b/src/app/styles/CustomHtml.css.ts
index 51f841b7..acf2c079 100644
--- a/src/app/styles/CustomHtml.css.ts
+++ b/src/app/styles/CustomHtml.css.ts
@@ -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',
});