diff --git a/.changeset/styling-state-event-replies.md b/.changeset/styling-state-event-replies.md new file mode 100644 index 00000000..7f6b2a6c --- /dev/null +++ b/.changeset/styling-state-event-replies.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Added styling for replies to non-messages. diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 1e047e9e..16f9f191 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -1,5 +1,5 @@ -import { Box, Chip, Icon, Icons, Text, as, color, toRem } from 'folds'; -import { EventTimelineSet, Room } from '$types/matrix-sdk'; +import { Box, Chip, Icon, IconSrc, Icons, Text, as, color, toRem } from 'folds'; +import { EventTimelineSet, Room, SessionMembershipData } from '$types/matrix-sdk'; import { MouseEventHandler, ReactNode, useCallback, useMemo } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import classNames from 'classnames'; @@ -19,6 +19,10 @@ import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; import { useIgnoredUsers } from '$hooks/useIgnoredUsers'; import { nicknamesAtom } from '$state/nicknames'; import { useMatrixClient } from '$hooks/useMatrixClient'; +import { useMemberEventParser } from '$hooks/useMemberEventParser'; +import { StateEvent, MessageEvent } from '$types/matrix/room'; +import { useTranslation } from 'react-i18next'; +import * as customHtmlCss from '$styles/CustomHtml.css'; import { MessageBadEncryptedContent, MessageBlockedContent, @@ -31,9 +35,10 @@ import { LinePlaceholder } from './placeholder'; type ReplyLayoutProps = { userColor?: string; username?: ReactNode; + icon?: IconSrc; }; export const ReplyLayout = as<'div', ReplyLayoutProps>( - ({ username, userColor, className, children, ...props }, ref) => ( + ({ username, userColor, icon, className, children, ...props }, ref) => ( ( {...props} ref={ref} > - + + + {!!icon && } + {username} @@ -88,19 +96,20 @@ export const Reply = as<'div', ReplyProps>( const { body, formatted_body: formattedBody, format } = replyEvent?.getContent() ?? {}; const sender = replyEvent?.getSender(); + const eventType = replyEvent?.getType(); const ignoredUsers = useIgnoredUsers(); const isBlockedSender = !!sender && ignoredUsers.includes(sender); + const { t } = useTranslation(); + const isRedacted = replyEvent?.isRedacted() === true; + + const parseMemberEvent = useMemberEventParser(); const { color: usernameColor, font: usernameFont } = useSableCosmetics(sender ?? '', room); const nicknames = useAtomValue(nicknamesAtom); const useAuthentication = useMediaAuthentication(); - const fallbackBody = replyEvent?.isRedacted() ? ( - - ) : ( - - ); + const fallbackBody = isRedacted ? : ; const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted'; @@ -116,6 +125,7 @@ export const Reply = as<'div', ReplyProps>( !replyEvent.getClearContent(); let bodyJSX: ReactNode = fallbackBody; + let image: IconSrc | undefined; if (format === 'org.matrix.custom.html' && formattedBody) { const strippedHtml = trimReplyFromFormattedBody(formattedBody) @@ -134,8 +144,33 @@ export const Reply = as<'div', ReplyProps>( } else if (body) { const strippedBody = trimReplyFromBody(body).replaceAll(/(?:\r\n|\r|\n)/g, ' '); bodyJSX = scaleSystemEmoji(strippedBody); + } else if (eventType === StateEvent.RoomMember && !!replyEvent) { + const parsedMemberEvent = parseMemberEvent(replyEvent); + image = parsedMemberEvent.icon; + bodyJSX = parsedMemberEvent.body; + } else if (eventType === StateEvent.RoomName) { + image = Icons.Hash; + bodyJSX = t('Organisms.RoomCommon.changed_room_name'); + } else if (eventType === StateEvent.RoomTopic) { + image = Icons.Hash; + bodyJSX = ' changed room topic'; + } else if (eventType === StateEvent.RoomAvatar) { + image = Icons.Hash; + bodyJSX = ' changed room avatar'; + } else if (eventType === StateEvent.GroupCallMemberPrefix && !!replyEvent) { + const callJoined = replyEvent.getContent().application; + image = callJoined ? Icons.Phone : Icons.PhoneDown; + bodyJSX = callJoined ? ' joined the call' : ' ended the call'; + } else if (Object.values(MessageEvent).every((v) => v !== eventType)) { + image = Icons.Code; + bodyJSX = ( + <> + {' sent '} + {eventType} + {' state event'} + + ); } - return ( {threadRootId && ( @@ -144,8 +179,10 @@ export const Reply = as<'div', ReplyProps>( {getMemberDisplayName(room, sender, nicknames) ?? getMxIdLocalPart(sender)} @@ -163,13 +200,15 @@ export const Reply = as<'div', ReplyProps>( })()} ) : ( - + (isRedacted && ) || ( + + ) )} {replyEvent === null && ( diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index ba0a5fd4..607035a1 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -3,7 +3,6 @@ import { KeyboardEventHandler, MouseEvent, RefObject, - ReactNode, useCallback, useEffect, useRef, @@ -44,13 +43,6 @@ import { toRem, } from 'folds'; -import parse from 'html-react-parser'; -import { - getReactCustomHtmlParser, - LINKIFY_OPTS, - scaleSystemEmoji, -} from '$plugins/react-custom-html-parser'; - import { useMatrixClient } from '$hooks/useMatrixClient'; import { AutocompletePrefix, @@ -83,7 +75,6 @@ import { TUploadContent, encryptFile, getImageInfo, - getMxIdLocalPart, mxcUrlToHttp, toggleReaction, } from '$utils/matrix'; @@ -113,23 +104,16 @@ import { safeFile } from '$utils/mimeTypes'; import { fulfilledPromiseSettledResult } from '$utils/common'; import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; -import { - getMemberDisplayName, - getMentionContent, - reactionOrEditEvent, - trimReplyFromBody, - trimReplyFromFormattedBody, -} from '$utils/room'; +import { getMentionContent, reactionOrEditEvent } from '$utils/room'; import { Command, SHRUG, TABLEFLIP, UNFLIP, useCommands } from '$hooks/useCommands'; import { mobileOrTablet } from '$utils/user-agent'; import { useElementSizeObserver } from '$hooks/useElementSizeObserver'; -import { ReplyLayout, ThreadIndicator } from '$components/message'; +import { Reply, ThreadIndicator } from '$components/message'; import { roomToParentsAtom } from '$state/room/roomToParents'; import { nicknamesAtom } from '$state/nicknames'; import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; import { useImagePackRooms } from '$hooks/useImagePackRooms'; import { useComposingCheck } from '$hooks/useComposingCheck'; -import { useSableCosmetics } from '$hooks/useSableCosmetics'; import { createLogger } from '$utils/debug'; import { createDebugLogger } from '$utils/debugLogger'; import FocusTrap from 'focus-trap-react'; @@ -275,12 +259,6 @@ export const RoomInput = forwardRef( const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(draftKey)); const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(draftKey)); - const replyUserID = replyDraft?.userId; - - const { color: replyUsernameColor, font: replyUsernameFont } = useSableCosmetics( - replyUserID ?? '', - room - ); const [uploadBoard, setUploadBoard] = useState(true); const [selectedFiles, setSelectedFiles] = useAtom(roomIdToUploadItemsAtomFamily(draftKey)); @@ -373,41 +351,6 @@ export const RoomInput = forwardRef( ); const replyEvent = replyDraft ? room.findEventById(replyDraft.eventId) : undefined; - const { - body: replyBody, - formatted_body: replyFormattedBody, - format: replyFormat, - } = replyEvent?.getContent() ?? {}; - - // Prefer the live event content; fall back to what was snapshotted in the - // draft when the user hit Reply (the event may not be in SDK state if it - // was redacted or evicted, but the draft always carries the original body). - const htmlBody = - replyFormat === 'org.matrix.custom.html' ? replyFormattedBody : replyDraft?.formattedBody; - const plainBody = replyBody ?? replyDraft?.body; - - let replyBodyJSX: ReactNode = replyDraft ? trimReplyFromBody(replyDraft.body) : null; - - if (htmlBody) { - /** - * message with linebreaks, etc stripped - */ - const strippedHtml = trimReplyFromFormattedBody(htmlBody) - .replaceAll(//gi, ' ') - .replaceAll(/<\/p>\s*]*>/gi, ' ') - .replaceAll(/<\/?p[^>]*>/gi, '') - .replaceAll(/(?:\r\n|\r|\n)/g, ' ') - .trim(); - const parserOpts = getReactCustomHtmlParser(mx, roomId, { - linkifyOpts: LINKIFY_OPTS, - useAuthentication, - nicknames, - }); - replyBodyJSX = parse(strippedHtml, parserOpts); - } else if (plainBody) { - const strippedBody = trimReplyFromBody(plainBody).replaceAll(/(?:\r\n|\r|\n)/g, ' '); - replyBodyJSX = scaleSystemEmoji(strippedBody); - } // Seed the reply draft with the thread relation whenever we're in thread // mode (e.g. on first render or when the thread root changes). We use the @@ -1260,22 +1203,7 @@ export const RoomInput = forwardRef( {replyDraft.relation?.rel_type === RelationType.Thread && !threadRootId && ( )} - - - {getMemberDisplayName(room, replyDraft.userId, nicknames) ?? - getMxIdLocalPart(replyDraft.userId) ?? - replyDraft.userId} - - - } - > - - {replyBodyJSX} - - + ( }} > + } + radii="300" + data-event-id={mEvent.getId()} + onClick={(evt: any) => { + onReplyClick(evt); + closeMenu(); + }} + > + + Reply + + {!hideReadReceipts && (