diff --git a/.changeset/added_message_history.md b/.changeset/added_message_history.md new file mode 100644 index 000000000..1bfd3f112 --- /dev/null +++ b/.changeset/added_message_history.md @@ -0,0 +1,5 @@ +--- +sable: minor +--- + +Added a pop-up for showing a message's edit history 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..e50d71c3a --- /dev/null +++ b/src/app/components/event-history/EventHistory.css.ts @@ -0,0 +1,33 @@ +import { style } from '@vanilla-extract/css'; +import { DefaultReset, color, 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, +}); +export const EventItem = style({ + padding: `${config.space.S200} ${config.space.S200}`, + height: 'unset', + borderRadius: '5px', + border: '2px hidden', + backgroundColor: 'inherit', + selectors: { + '&:hover': { + backgroundColor: color.Surface.ContainerHover, + }, + }, +}); diff --git a/src/app/components/event-history/EventHistory.tsx b/src/app/components/event-history/EventHistory.tsx new file mode 100644 index 000000000..ffdefa1ee --- /dev/null +++ b/src/app/components/event-history/EventHistory.tsx @@ -0,0 +1,159 @@ +import classNames from 'classnames'; +import { + Avatar, + Box, + Header, + Icon, + IconButton, + Icons, + MenuItem, + Scroll, + Text, + as, + color, + 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 { 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 = { + 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; + + 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'); + + 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 ( + +
+ + Message version history + + + + +
+
+ { + openProfile( + room.roomId, + space?.roomId, + readerId, + getMouseEventCords(event.nativeEvent), + 'Bottom' + ); + }} + before={ + + } + /> + + } + > + {name} + +
+ + + + {mEvents.map((mEvent) => { + if (!mEvent.event.sender) return
; + const EventContent = mEvent.getOriginalContent(); + return ( + <> +
+ + + + ); + })} + + + + + ); + } +); 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]; + edits.sort((a, b) => a.getTs() - b.getTs()); + return [mEvent, ...edits]; + }; + + return ; +} diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index a027d12ed..f20f6a18d 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -49,7 +49,7 @@ import { Username, UsernameBold, } from '$components/message'; -import { canEditEvent, getMemberAvatarMxc } from '$utils/room'; +import { canEditEvent, getEventEdits, getMemberAvatarMxc } from '$utils/room'; import { mxcUrlToHttp } from '$utils/matrix'; import { getSettings, MessageLayout, MessageSpacing, settingsAtom } from '$state/settings'; import { nicknamesAtom, setNicknameAtom } from '$state/nicknames'; @@ -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'; @@ -667,6 +668,13 @@ function MessageInternal( const isThreadedMessage = mEvent.threadRootId !== undefined; + const evtId = mEvent.getId()!; + const evtTimeline = room.getTimelineForEvent(evtId); + const edits = + evtTimeline && + getEventEdits(evtTimeline.getTimelineSet(), evtId, mEvent.getType())?.getRelations(); + const isEdited = edits !== undefined; + return ( )} + {isEdited && } {showDeveloperTools && ( )} @@ -1124,6 +1133,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 && ( )} 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;