Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/added_message_history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
sable: minor
---

Added a pop-up for showing a message's edit history
33 changes: 33 additions & 0 deletions src/app/components/event-history/EventHistory.css.ts
Original file line number Diff line number Diff line change
@@ -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,
},
},
});
159 changes: 159 additions & 0 deletions src/app/components/event-history/EventHistory.tsx
Original file line number Diff line number Diff line change
@@ -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<LinkifyOpts>(() => ({ ...LINKIFY_OPTS }), []);
const spoilerClickHandler = useSpoilerClickHandler();
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
() =>
getReactCustomHtmlParser(mx, mEvents[0].getRoomId(), {
linkifyOpts,
useAuthentication,
handleSpoilerClick: spoilerClickHandler,
}),
[linkifyOpts, mEvents, mx, spoilerClickHandler, useAuthentication]
);
return (
<Box
className={classNames(css.EventHistory, className)}
direction="Column"
{...props}
ref={ref}
>
<Header className={css.Header} variant="Surface" size="600">
<Box grow="Yes">
<Text size="H3">Message version history</Text>
</Box>
<IconButton size="300" onClick={requestClose}>
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Header>
<MenuItem
key={readerId}
style={{
display: 'flex',
justifyContent: 'center',
padding: `${config.space.S200} ${config.space.S200}`,
height: 'unset',
}}
radii="400"
onClick={(event) => {
openProfile(
room.roomId,
space?.roomId,
readerId,
getMouseEventCords(event.nativeEvent),
'Bottom'
);
}}
before={
<Avatar size="300">
<UserAvatar
userId={readerId ?? ''}
src={avatarUrl ?? undefined}
alt={name}
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
/>
</Avatar>
}
>
<Text size="T400">{name}</Text>
</MenuItem>
</Header>
<Box grow="Yes" style={{ overflow: 'scroll' }}>
<Scroll visibility="Hover">
<Box className={css.Content} direction="Column">
{mEvents.map((mEvent) => {
if (!mEvent.event.sender) return <div />;
const EventContent = mEvent.getOriginalContent();
return (
<>
<hr style={{ width: '100%', color: color.Surface.ContainerLine }} />
<Box className={css.EventItem}>
<Time
ts={mEvent.getTs()}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
/>
<Text size="T400" style={{ paddingLeft: '10px', wordBreak: 'break-word' }}>
<RenderBody
body={EventContent?.['m.new_content']?.body ?? EventContent?.body ?? ''}
customBody={
EventContent?.['m.new_content']?.formatted_body ??
EventContent?.formatted_body ??
''
}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/>
</Text>
</Box>
</>
);
})}
</Box>
</Scroll>
</Box>
</Box>
);
}
);
1 change: 1 addition & 0 deletions src/app/components/event-history/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './EventHistory';
11 changes: 11 additions & 0 deletions src/app/components/message/modals/GlobalModalManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -63,6 +64,16 @@ export function GlobalModalManager() {
</Modal>
)}

{modal.type === ModalType.EditHistory && (
<Modal variant="Surface" size="300">
<MessageEditHistoryInternal
room={modal.room}
mEvent={modal.mEvent}
onClose={close}
/>
</Modal>
)}

{modal.type === ModalType.ReadReceipts && (
<Modal variant="Surface" size="300">
<MessageReadReceiptInternal
Expand Down
58 changes: 58 additions & 0 deletions src/app/components/message/modals/MessageEditHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { MouseEvent } from 'react';
import { Room, MatrixEvent } from '$types/matrix-sdk';
import { useSetAtom } from 'jotai';
import { MenuItem, Icon, Icons, Text } from 'folds';
import { getEventEdits } from '$utils/room';
import { modalAtom, ModalType } from '$state/modal';
import * as css from '$features/room/message/styles.css';
import { EventHistory } from '$components/event-history';

export function MessageEditHistoryItem({ room, mEvent }: { room: Room; mEvent: MatrixEvent }) {
const setModal = useSetAtom(modalAtom);

return (
<MenuItem
size="300"
after={<Icon size="100" src={Icons.Clock} />}
radii="300"
onClick={(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setModal({
type: ModalType.EditHistory,
room,
mEvent,
});
}}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
Version History
</Text>
</MenuItem>
);
}

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 <EventHistory room={room} mEvents={getEvents()} requestClose={onClose} />;
}
19 changes: 18 additions & 1 deletion src/app/features/room/message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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 (
<MessageBase
className={classNames(css.MessageBase, className, {
Expand Down Expand Up @@ -869,6 +877,7 @@ function MessageInternal(
{!hideReadReceipts && (
<MessageReadReceiptItem room={room} eventId={mEvent.getId() ?? ''} />
)}
{isEdited && <MessageEditHistoryItem room={room} mEvent={mEvent} />}
{showDeveloperTools && (
<MessageSourceCodeItem room={room} mEvent={mEvent} />
)}
Expand Down Expand Up @@ -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 (
<MessageBase
className={classNames(css.MessageBase, className)}
Expand Down Expand Up @@ -1163,6 +1179,7 @@ export const Event = as<'div', EventProps>(
{!hideReadReceipts && (
<MessageReadReceiptItem room={room} eventId={mEvent.getId() ?? ''} />
)}
{isEdited && <MessageEditHistoryItem room={room} mEvent={mEvent} />}
{showDeveloperTools && (
<MessageSourceCodeItem room={room} mEvent={mEvent} />
)}
Expand Down
2 changes: 2 additions & 0 deletions src/app/state/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum ModalType {
Report = 'report',
Source = 'source',
Reactions = 'reactions',
EditHistory = 'edit_history',
ReadReceipts = 'read_receipts',
}

Expand All @@ -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;
Expand Down
Loading