From 5edd42698e4755cc70e857a19e15ec19a1075ca3 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Sun, 8 Mar 2026 22:04:31 +0200 Subject: [PATCH 1/9] basic message history --- .../event-history/EventHistory.css.ts | 21 ++++ .../components/event-history/EventHistory.tsx | 116 ++++++++++++++++++ src/app/components/event-history/index.ts | 1 + .../message/modals/GlobalModalManager.tsx | 11 ++ .../message/modals/MessageEditHistory.tsx | 58 +++++++++ src/app/features/room/message/Message.tsx | 3 + src/app/state/modal.ts | 2 + 7 files changed, 212 insertions(+) create mode 100644 src/app/components/event-history/EventHistory.css.ts create mode 100644 src/app/components/event-history/EventHistory.tsx create mode 100644 src/app/components/event-history/index.ts create mode 100644 src/app/components/message/modals/MessageEditHistory.tsx diff --git a/src/app/components/event-history/EventHistory.css.ts b/src/app/components/event-history/EventHistory.css.ts new file mode 100644 index 000000000..ff044e61b --- /dev/null +++ b/src/app/components/event-history/EventHistory.css.ts @@ -0,0 +1,21 @@ +import { style } from '@vanilla-extract/css'; +import { DefaultReset, config } from 'folds'; + +export const EventHistory = style([ + DefaultReset, + { + height: '100%', + }, +]); + +export const Header = style({ + paddingLeft: config.space.S400, + paddingRight: config.space.S300, + + flexShrink: 0, +}); + +export const Content = style({ + paddingLeft: config.space.S200, + paddingBottom: config.space.S400, +}); diff --git a/src/app/components/event-history/EventHistory.tsx b/src/app/components/event-history/EventHistory.tsx new file mode 100644 index 000000000..e08d4ebd2 --- /dev/null +++ b/src/app/components/event-history/EventHistory.tsx @@ -0,0 +1,116 @@ +import classNames from 'classnames'; +import { + Avatar, + Box, + Header, + Icon, + IconButton, + Icons, + MenuItem, + Scroll, + Text, + as, + config, +} from 'folds'; +import { MatrixEvent, Room } from '$types/matrix-sdk'; +import { getMemberDisplayName } from '$utils/room'; +import { getMxIdLocalPart } from '$utils/matrix'; +import { useMatrixClient } from '$hooks/useMatrixClient'; +import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; +import { useOpenUserRoomProfile } from '$state/hooks/userRoomProfile'; +import { useSpaceOptionally } from '$hooks/useSpace'; +import { getMouseEventCords } from '$utils/dom'; +import { useAtomValue } from 'jotai'; +import { nicknamesAtom } from '$state/nicknames'; +import { UserAvatar } from '$components/user-avatar'; +import * as css from './EventHistory.css'; + +export type EventHistoryProps = { + room: Room; + mEvents: MatrixEvent[]; + requestClose: () => void; +}; +export const EventHistory = as<'div', EventHistoryProps>( + ({ className, room, mEvents, requestClose, ...props }, ref) => { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const openProfile = useOpenUserRoomProfile(); + const space = useSpaceOptionally(); + const nicknames = useAtomValue(nicknamesAtom); + + const getName = (userId: string) => + getMemberDisplayName(room, userId, nicknames) ?? getMxIdLocalPart(userId) ?? userId; + + return ( + +
+ + Message version history + + + + +
+ + + + {mEvents.map((mEvent) => { + if (!mEvent.event.sender) return
; + const readerId = mEvent.event.sender; + const name = getName(readerId); + const avatarMxcUrl = room.getMember(readerId)?.getMxcAvatarUrl(); + const avatarUrl = avatarMxcUrl + ? mx.mxcUrlToHttp( + avatarMxcUrl, + 100, + 100, + 'crop', + undefined, + false, + useAuthentication + ) + : undefined; + + return ( + { + openProfile( + room.roomId, + space?.roomId, + readerId, + getMouseEventCords(event.nativeEvent), + 'Bottom' + ); + }} + before={ + + } + /> + + } + > + + {name}·{mEvent.event?.content?.body ?? ''} + + + ); + })} + + + + + ); + } +); diff --git a/src/app/components/event-history/index.ts b/src/app/components/event-history/index.ts new file mode 100644 index 000000000..0ff43225b --- /dev/null +++ b/src/app/components/event-history/index.ts @@ -0,0 +1 @@ +export * from './EventHistory'; diff --git a/src/app/components/message/modals/GlobalModalManager.tsx b/src/app/components/message/modals/GlobalModalManager.tsx index 5245e98bf..d422d6e60 100644 --- a/src/app/components/message/modals/GlobalModalManager.tsx +++ b/src/app/components/message/modals/GlobalModalManager.tsx @@ -5,6 +5,7 @@ import { stopPropagation } from '$utils/keyboard'; import { modalAtom, ModalType } from '$state/modal'; import { MessageReportInternal } from './MessageReport'; import { MessageDeleteInternal } from './MessageDelete'; +import { MessageEditHistoryInternal } from './MessageEditHistory'; import { MessageSourceInternal } from './MessageSource'; import { MessageForwardInternal } from './MessageForward'; import { MessageAllReactionInternal } from './MessageReactions'; @@ -63,6 +64,16 @@ export function GlobalModalManager() { )} + {modal.type === ModalType.EditHistory && ( + + + + )} + {modal.type === ModalType.ReadReceipts && ( } + radii="300" + onClick={(e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setModal({ + type: ModalType.EditHistory, + room, + mEvent, + }); + }} + > + + Version History + + + ); +} + +type MessageEditHistoryInternalProps = { + room: Room; + mEvent: MatrixEvent; + onClose: () => void; +}; + +export function MessageEditHistoryInternal({ + room, + mEvent, + onClose, +}: MessageEditHistoryInternalProps) { + const getEvents = (): MatrixEvent[] => { + const evtId = mEvent.getId()!; + const evtTimeline = room.getTimelineForEvent(evtId); + const edits = + evtTimeline && + getEventEdits(evtTimeline.getTimelineSet(), evtId, mEvent.getType())?.getRelations(); + if (!edits) return [mEvent]; + return [mEvent, ...edits]; + }; + // console.log(getEvents().map((m) => m.event.content)); + + return ; +} diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index a556ee0a1..5c2328bc9 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -74,6 +74,7 @@ import { useSetting } from '$state/hooks/settings'; import { useBlobCache } from '$hooks/useBlobCache'; import { MessageAllReactionItem } from '$components/message/modals/MessageReactions'; import { MessageReadReceiptItem } from '$components/message/modals/MessageReadRecipts'; +import { MessageEditHistoryItem } from '$components/message/modals/MessageEditHistory'; import { MessageSourceCodeItem } from '$components/message/modals/MessageSource'; import { MessageForwardItem } from '$components/message/modals/MessageForward'; import { MessageDeleteItem } from '$components/message/modals/MessageDelete'; @@ -852,6 +853,7 @@ function MessageInternal( {!hideReadReceipts && ( )} + {showDeveloperTools && ( )} @@ -1146,6 +1148,7 @@ export const Event = as<'div', EventProps>( {!hideReadReceipts && ( )} + {showDeveloperTools && ( )} diff --git a/src/app/state/modal.ts b/src/app/state/modal.ts index c4f4cf79b..5b54dc637 100644 --- a/src/app/state/modal.ts +++ b/src/app/state/modal.ts @@ -8,6 +8,7 @@ export enum ModalType { Report = 'report', Source = 'source', Reactions = 'reactions', + EditHistory = 'edit_history', ReadReceipts = 'read_receipts', } @@ -16,6 +17,7 @@ export type ModalState = | { type: ModalType.Forward; room: Room; mEvent: MatrixEvent } | { type: ModalType.Report; room: Room; mEvent: MatrixEvent } | { type: ModalType.Source; room: Room; mEvent: MatrixEvent } + | { type: ModalType.EditHistory; room: Room; mEvent: MatrixEvent } | { type: ModalType.Reactions; room: Room; relations: Relations } | { type: ModalType.ReadReceipts; room: Room; eventId: string } | null; From 90a8f91e326268b7d48fe974fb9db365a2cc60ca Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Sun, 8 Mar 2026 23:48:11 +0200 Subject: [PATCH 2/9] aesthetically fine message history --- .../components/event-history/EventHistory.tsx | 89 ++++++++++++------- .../message/modals/MessageEditHistory.tsx | 1 - 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/app/components/event-history/EventHistory.tsx b/src/app/components/event-history/EventHistory.tsx index e08d4ebd2..cb0ab219c 100644 --- a/src/app/components/event-history/EventHistory.tsx +++ b/src/app/components/event-history/EventHistory.tsx @@ -23,6 +23,9 @@ import { getMouseEventCords } from '$utils/dom'; import { useAtomValue } from 'jotai'; import { nicknamesAtom } from '$state/nicknames'; import { UserAvatar } from '$components/user-avatar'; +import { Time } from '$components/message'; +import { useSetting } from '$state/hooks/settings'; +import { settingsAtom } from '$state/settings'; import * as css from './EventHistory.css'; export type EventHistoryProps = { @@ -41,6 +44,16 @@ export const EventHistory = as<'div', EventHistoryProps>( const getName = (userId: string) => getMemberDisplayName(room, userId, nicknames) ?? getMxIdLocalPart(userId) ?? userId; + const readerId = mEvents[0].event.sender ?? ''; + const name = getName(readerId ?? ''); + const avatarMxcUrl = room.getMember(readerId ?? '')?.getMxcAvatarUrl(); + const avatarUrl = avatarMxcUrl + ? mx.mxcUrlToHttp(avatarMxcUrl, 100, 100, 'crop', undefined, false, useAuthentication) + : undefined; + + const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); + const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString'); + return ( ( +
+ { + openProfile( + room.roomId, + space?.roomId, + readerId, + getMouseEventCords(event.nativeEvent), + 'Bottom' + ); + }} + before={ + + } + /> + + } + > + {name} + +
- + {mEvents.map((mEvent) => { if (!mEvent.event.sender) return
; - const readerId = mEvent.event.sender; - const name = getName(readerId); - const avatarMxcUrl = room.getMember(readerId)?.getMxcAvatarUrl(); - const avatarUrl = avatarMxcUrl - ? mx.mxcUrlToHttp( - avatarMxcUrl, - 100, - 100, - 'crop', - undefined, - false, - useAuthentication - ) - : undefined; return ( { - openProfile( - room.roomId, - space?.roomId, - readerId, - getMouseEventCords(event.nativeEvent), - 'Bottom' - ); - }} before={ - - } - /> - + ); diff --git a/src/app/components/message/modals/MessageEditHistory.tsx b/src/app/components/message/modals/MessageEditHistory.tsx index 4bdded5ef..c07f637da 100644 --- a/src/app/components/message/modals/MessageEditHistory.tsx +++ b/src/app/components/message/modals/MessageEditHistory.tsx @@ -52,7 +52,6 @@ export function MessageEditHistoryInternal({ if (!edits) return [mEvent]; return [mEvent, ...edits]; }; - // console.log(getEvents().map((m) => m.event.content)); return ; } From 7b7c60a7d1c9deaa8ff5349b9572332a508fe134 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Mon, 9 Mar 2026 02:37:23 +0200 Subject: [PATCH 3/9] made message history conditional --- .../components/event-history/EventHistory.tsx | 8 ++++++-- src/app/features/room/message/Message.tsx | 20 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/app/components/event-history/EventHistory.tsx b/src/app/components/event-history/EventHistory.tsx index cb0ab219c..8d7c5abef 100644 --- a/src/app/components/event-history/EventHistory.tsx +++ b/src/app/components/event-history/EventHistory.tsx @@ -75,7 +75,8 @@ export const EventHistory = as<'div', EventHistoryProps>( style={{ display: 'flex', justifyContent: 'center', - padding: `0 ${config.space.S200}`, + padding: `${config.space.S200} ${config.space.S200}`, + height: 'unset', }} radii="400" onClick={(event) => { @@ -110,7 +111,10 @@ export const EventHistory = as<'div', EventHistoryProps>( return ( )} - + {isEdited && } {showDeveloperTools && ( )} @@ -1109,6 +1116,13 @@ export const Event = as<'div', EventProps>( setMobileOptionsOpen(true); }); + const evtId = mEvent.getId()!; + const evtTimeline = room.getTimelineForEvent(evtId); + const edits = + evtTimeline && + getEventEdits(evtTimeline.getTimelineSet(), evtId, mEvent.getType())?.getRelations(); + const isEdited = edits !== undefined; + return ( ( {!hideReadReceipts && ( )} - + {isEdited && } {showDeveloperTools && ( )} From c63727e2328a6f41d50dc012940f67b96ff0f13c Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Mon, 9 Mar 2026 03:02:26 +0200 Subject: [PATCH 4/9] added version history formatting --- .../components/event-history/EventHistory.tsx | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/app/components/event-history/EventHistory.tsx b/src/app/components/event-history/EventHistory.tsx index 8d7c5abef..ea1168730 100644 --- a/src/app/components/event-history/EventHistory.tsx +++ b/src/app/components/event-history/EventHistory.tsx @@ -23,9 +23,14 @@ import { getMouseEventCords } from '$utils/dom'; import { useAtomValue } from 'jotai'; import { nicknamesAtom } from '$state/nicknames'; import { UserAvatar } from '$components/user-avatar'; -import { Time } from '$components/message'; +import { RenderBody, Time } from '$components/message'; import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; +import { useMemo } from 'react'; +import { getReactCustomHtmlParser, LINKIFY_OPTS } from '$plugins/react-custom-html-parser'; +import { Opts as LinkifyOpts } from 'linkifyjs'; +import { HTMLReactParserOptions } from 'html-react-parser'; +import { useSpoilerClickHandler } from '$hooks/useSpoilerClickHandler'; import * as css from './EventHistory.css'; export type EventHistoryProps = { @@ -54,6 +59,17 @@ export const EventHistory = as<'div', EventHistoryProps>( const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString'); + const linkifyOpts = useMemo(() => ({ ...LINKIFY_OPTS }), []); + const spoilerClickHandler = useSpoilerClickHandler(); + const htmlReactParserOptions = useMemo( + () => + getReactCustomHtmlParser(mx, mEvents[0].getRoomId(), { + linkifyOpts, + useAuthentication, + handleSpoilerClick: spoilerClickHandler, + }), + [linkifyOpts, mEvents, mx, spoilerClickHandler, useAuthentication] + ); return ( ( {name} - + {mEvents.map((mEvent) => { @@ -124,10 +140,21 @@ export const EventHistory = as<'div', EventHistoryProps>( /> } > - - {mEvent?.event?.content?.['m.new_content']?.body ?? - mEvent.event?.content?.body ?? - ''} + + ); From d16e431c743a622d7ba0103261666fb36448b3a9 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Mon, 9 Mar 2026 03:42:29 +0200 Subject: [PATCH 5/9] added borders on hover --- .../event-history/EventHistory.css.ts | 14 ++++++++++- .../components/event-history/EventHistory.tsx | 25 ++++++------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/app/components/event-history/EventHistory.css.ts b/src/app/components/event-history/EventHistory.css.ts index ff044e61b..277d220a1 100644 --- a/src/app/components/event-history/EventHistory.css.ts +++ b/src/app/components/event-history/EventHistory.css.ts @@ -1,5 +1,5 @@ import { style } from '@vanilla-extract/css'; -import { DefaultReset, config } from 'folds'; +import { DefaultReset, color, config } from 'folds'; export const EventHistory = style([ DefaultReset, @@ -19,3 +19,15 @@ export const Content = style({ paddingLeft: config.space.S200, paddingBottom: config.space.S400, }); +export const EventItem = style({ + padding: `${config.space.S200} ${config.space.S200}`, + height: 'unset', + borderRadius: '5px', + border: '2px hidden', + borderColor: color.Secondary.Main, + selectors: { + '&:hover': { + border: '2px solid', + }, + }, +}); diff --git a/src/app/components/event-history/EventHistory.tsx b/src/app/components/event-history/EventHistory.tsx index ea1168730..12c80849f 100644 --- a/src/app/components/event-history/EventHistory.tsx +++ b/src/app/components/event-history/EventHistory.tsx @@ -125,22 +125,13 @@ export const EventHistory = as<'div', EventHistoryProps>( if (!mEvent.event.sender) return
; return ( - - } - > - + + + ); })} From a94f154f338fa029c0a450d7c9032ff9ba221cf4 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Mon, 9 Mar 2026 03:46:56 +0200 Subject: [PATCH 6/9] added md --- .changeset/added_message_history.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/added_message_history.md diff --git a/.changeset/added_message_history.md b/.changeset/added_message_history.md new file mode 100644 index 000000000..bb318cc46 --- /dev/null +++ b/.changeset/added_message_history.md @@ -0,0 +1,5 @@ +--- +sable: minor +--- + +Added a pop-up for showing a message's history From e933a456e8caff1bead46701e9aad12e680604b4 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Mon, 9 Mar 2026 03:47:29 +0200 Subject: [PATCH 7/9] fixed md --- .changeset/added_message_history.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/added_message_history.md b/.changeset/added_message_history.md index bb318cc46..1bfd3f112 100644 --- a/.changeset/added_message_history.md +++ b/.changeset/added_message_history.md @@ -2,4 +2,4 @@ sable: minor --- -Added a pop-up for showing a message's history +Added a pop-up for showing a message's edit history From b09709deb63c8a9314491e77d2174aeeea1386f1 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Mon, 9 Mar 2026 05:13:23 +0200 Subject: [PATCH 8/9] added encrypted room support --- src/app/components/event-history/EventHistory.tsx | 12 ++++-------- .../components/message/modals/MessageEditHistory.tsx | 1 + 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/app/components/event-history/EventHistory.tsx b/src/app/components/event-history/EventHistory.tsx index 12c80849f..52904e632 100644 --- a/src/app/components/event-history/EventHistory.tsx +++ b/src/app/components/event-history/EventHistory.tsx @@ -123,7 +123,7 @@ export const EventHistory = as<'div', EventHistoryProps>( {mEvents.map((mEvent) => { if (!mEvent.event.sender) return
; - + const EventContent = mEvent.getOriginalContent(); return (