diff --git a/.changeset/pr465-reply-mention-indicator.md b/.changeset/pr465-reply-mention-indicator.md new file mode 100644 index 000000000..dbc5c876a --- /dev/null +++ b/.changeset/pr465-reply-mention-indicator.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Replies that mention the OP are now indicated by the OP username being prefixed with @ diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 5ae0ebdec..f67db051f 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -1,5 +1,5 @@ import { Box, Chip, Icon, IconSrc, Icons, Text, as, color, toRem } from 'folds'; -import { EventTimelineSet, Room, SessionMembershipData } from '$types/matrix-sdk'; +import { EventTimelineSet, IMentions, Room, SessionMembershipData } from '$types/matrix-sdk'; import { MouseEventHandler, ReactNode, useCallback, useMemo } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import classNames from 'classnames'; @@ -40,9 +40,10 @@ type ReplyLayoutProps = { userColor?: string; username?: ReactNode; icon?: IconSrc; + mentioned: boolean; }; export const ReplyLayout = as<'div', ReplyLayoutProps>( - ({ username, userColor, icon, className, children, ...props }, ref) => ( + ({ username, userColor, icon, className, mentioned, children, ...props }, ref) => ( ( {!!icon && } + {mentioned && } {username} @@ -83,11 +85,12 @@ type ReplyProps = { timelineSet?: EventTimelineSet; replyEventId: string; threadRootId?: string; + mentions?: IMentions; onClick?: MouseEventHandler; }; export const Reply = as<'div', ReplyProps>( - ({ room, timelineSet, replyEventId, threadRootId, onClick, ...props }, ref) => { + ({ room, timelineSet, replyEventId, threadRootId, mentions, onClick, ...props }, ref) => { const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []); const getFromLocalTimeline = useCallback( () => timelineSet?.findEventById(replyEventId), @@ -131,6 +134,7 @@ export const Reply = as<'div', ReplyProps>( let bodyJSX: ReactNode = fallbackBody; let image: IconSrc | undefined; + let mentioned = sender != null && (mentions?.user_ids?.includes(sender) ?? false); const replyLinkifyOpts = useMemo( () => ({ @@ -169,6 +173,7 @@ export const Reply = as<'div', ReplyProps>( } else if (eventType === StateEvent.RoomMember && !!replyEvent) { const parsedMemberEvent = parseMemberEvent(replyEvent); image = parsedMemberEvent.icon; + mentioned = false; bodyJSX = parsedMemberEvent.body; } else if (eventType === StateEvent.RoomName) { image = Icons.Hash; @@ -202,6 +207,7 @@ export const Reply = as<'div', ReplyProps>( as="button" userColor={usernameColor} icon={image} + mentioned={mentioned} username={ sender && eventType !== StateEvent.RoomMember && ( diff --git a/src/app/features/message-search/SearchResultGroup.tsx b/src/app/features/message-search/SearchResultGroup.tsx index 86a67553c..7e4bcf533 100644 --- a/src/app/features/message-search/SearchResultGroup.tsx +++ b/src/app/features/message-search/SearchResultGroup.tsx @@ -323,6 +323,7 @@ export function SearchResultGroup({ room={room} replyEventId={replyEventId} threadRootId={threadRootId} + mentions={event.content['m.mentions']} onClick={handleOpenClick} /> )} diff --git a/src/app/features/room/ThreadBrowser.tsx b/src/app/features/room/ThreadBrowser.tsx index 2762cb688..7c1e830f0 100644 --- a/src/app/features/room/ThreadBrowser.tsx +++ b/src/app/features/room/ThreadBrowser.tsx @@ -185,6 +185,7 @@ function ThreadPreview({ room, thread, onClick }: ThreadPreviewProps) { room={room} replyEventId={rootEvent.replyEventId} threadRootId={rootEvent.threadRootId} + mentions={rootEvent.getContent()['m.mentions']} onClick={handleJumpClick} /> )} diff --git a/src/app/features/room/ThreadDrawer.tsx b/src/app/features/room/ThreadDrawer.tsx index d7a1fe63a..163d40ff0 100644 --- a/src/app/features/room/ThreadDrawer.tsx +++ b/src/app/features/room/ThreadDrawer.tsx @@ -255,6 +255,7 @@ function ThreadMessage({ room={room} timelineSet={timelineSet} replyEventId={replyEventId} + mentions={baseContent['m.mentions']} onClick={onReferenceClick} /> ) diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx index d05284da0..9762f216a 100644 --- a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx +++ b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx @@ -134,6 +134,7 @@ function PinnedMessageActiveContent( getMemberDisplayName(room, sender, nicknames) ?? getMxIdLocalPart(sender) ?? sender; const senderAvatarMxc = getMemberAvatarMxc(room, sender); const getContent = (() => pinnedEvent.getContent()) as GetContentCallback; + const content = pinnedEvent.getContent(); const memberPowerTag = getMemberPowerTag(sender); const tagIconSrc = memberPowerTag?.icon @@ -183,6 +184,7 @@ function PinnedMessageActiveContent( room={room} replyEventId={pinnedEvent.replyEventId} threadRootId={pinnedEvent.threadRootId} + mentions={content['m.mentions']} onClick={handleOpenClick} /> )} diff --git a/src/app/hooks/timeline/useTimelineEventRenderer.tsx b/src/app/hooks/timeline/useTimelineEventRenderer.tsx index eb525e818..86e703452 100644 --- a/src/app/hooks/timeline/useTimelineEventRenderer.tsx +++ b/src/app/hooks/timeline/useTimelineEventRenderer.tsx @@ -7,6 +7,7 @@ import { Room, PushProcessor, EventTimelineSet, + IContent, } from '$types/matrix-sdk'; import { SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc/CallMembership'; import { HTMLReactParserOptions } from 'html-react-parser'; @@ -294,10 +295,10 @@ export function useTimelineEventRenderer({ editedNewContent = getEditedContent.call(editedEvent)['m.new_content']; } - const baseContent = (getEventContent.call(mEvent) || {}) as Record; + const baseContent = getEventContent.call(mEvent) || {}; const safeContent = ( Object.keys(baseContent).length > 0 ? baseContent : getOriginalContent.call(mEvent) - ) as Record; + ) as IContent; const getContent = (() => editedNewContent ?? safeContent) as GetContentCallback; @@ -364,6 +365,7 @@ export function useTimelineEventRenderer({ timelineSet={timelineSet} replyEventId={replyEventId} threadRootId={threadRootId} + mentions={baseContent['m.mentions']} onClick={handleOpenReply} /> ) @@ -609,6 +611,7 @@ export function useTimelineEventRenderer({ const senderId = getSender.call(mEvent) ?? ''; const senderDisplayName = getMemberDisplayName(room, senderId, nicknames) ?? getMxIdLocalPart(senderId) ?? senderId; + const content = getEventContent.call(mEvent) ?? {}; return ( ) diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx index 7543720d0..dc3d35297 100644 --- a/src/app/pages/client/inbox/Notifications.tsx +++ b/src/app/pages/client/inbox/Notifications.tsx @@ -468,6 +468,9 @@ function RoomNotificationsGroupComp({ const replyEventId = relation?.['m.in_reply_to']?.event_id; const threadRootId = relation?.rel_type === RelationType.Thread ? relation.event_id : undefined; + // doesn't work for encrypted rooms + // not a big deal really, don't want to bother with finding the event by id and decrypting + const mentions = event.content['m.mentions']; const memberPowerTag = getMemberPowerTag(event.sender); const tagColor = memberPowerTag?.color @@ -543,6 +546,7 @@ function RoomNotificationsGroupComp({ room={room} replyEventId={replyEventId} threadRootId={threadRootId} + mentions={mentions} onClick={handleOpenClick} /> )}