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/improved_message_history_ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Added hover menu inside Message Version Pop-out.
25 changes: 24 additions & 1 deletion src/app/components/event-history/EventHistory.css.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { style } from '@vanilla-extract/css';
import { DefaultReset, color, config } from 'folds';
import { DefaultReset, color, config, toRem } from 'folds';

export const EventHistory = style([
DefaultReset,
Expand All @@ -22,6 +22,7 @@ export const Content = style({
export const EventItem = style({
padding: `${config.space.S200} ${config.space.S200}`,
height: 'unset',
width: '100%',
borderRadius: '5px',
border: '2px hidden',
backgroundColor: 'inherit',
Expand All @@ -31,3 +32,25 @@ export const EventItem = style({
},
},
});
export const MessageOptionsBase = style([
DefaultReset,
{
position: 'absolute',
top: toRem(-30),
right: 0,
zIndex: 1,
},
]);
export const MessageOptionsBar = style([
DefaultReset,
{
padding: config.space.S100,
},
]);
export const MenuOptions = style({
position: 'absolute',
right: '0',
top: '0',
display: 'flex',
transform: 'translateY(-75%)',
});
166 changes: 144 additions & 22 deletions src/app/components/event-history/EventHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,39 @@ import {
Icon,
IconButton,
Icons,
Menu,
MenuItem,
Scroll,
Text,
as,
color,
config,
} from 'folds';
import { MatrixEvent, Room } from '$types/matrix-sdk';
import { IContent, 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 { useAtomValue, useSetAtom } 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 { useCallback, useMemo, useState } 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 { modalAtom, ModalType } from '$state/modal';
import { roomIdToReplyDraftAtomFamily } from '$state/room/roomInputDrafts';
import { useRoomPermissions } from '$hooks/useRoomPermissions';
import { useRoomCreators } from '$hooks/useRoomCreators';
import { usePowerLevelsContext } from '$hooks/usePowerLevels';
import { MessageEvent } from '$types/matrix/room';
import * as css from './EventHistory.css';

export type EventHistoryProps = {
Expand Down Expand Up @@ -71,6 +78,139 @@ export const EventHistory = as<'div', EventHistoryProps>(
}),
[linkifyOpts, mEvents, mx, spoilerClickHandler, useAuthentication]
);
const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels);
const canRedact = permissions.action('redact', mx.getSafeUserId());
const canDeleteOwn = permissions.event(MessageEvent.RoomRedaction, mx.getSafeUserId());
const canDelete = canRedact || (canDeleteOwn && mEvents[0].getSender() === mx.getUserId());

const setReplyDraft = useSetAtom(roomIdToReplyDraftAtomFamily(room.roomId));
const triggerReply = useCallback(
(replyId: string, startThread = false) => {
const replyEvt = room.findEventById(replyId);
if (!replyEvt) return;
const content: IContent = replyEvt.getOriginalContent();
const body = content?.['m.new_content']?.body ?? content?.body ?? '';
const formattedBody =
content?.['m.new_content']?.formatted_body ?? content?.formatted_body ?? '';
const { 'm.relates_to': relation } = startThread
? { 'm.relates_to': { rel_type: 'm.thread', event_id: replyId } }
: replyEvt.getWireContent();
const senderId = replyEvt.getSender();
if (senderId) {
if (typeof body === 'string') {
setReplyDraft({
userId: senderId,
eventId: replyId,
body,
formattedBody,
relation,
});
} else {
setReplyDraft({
userId: senderId,
eventId: replyId,
body: '',
formattedBody: '',
relation,
});
}
}
},
[room, setReplyDraft]
);

function MenuOptions({ mEvent }: { mEvent: MatrixEvent }) {
const setModal = useSetAtom(modalAtom);
return (
<Menu className={css.MenuOptions}>
<MenuItem
size="300"
after={<Icon size="100" src={Icons.ReplyArrow} />}
radii="300"
fill="None"
variant="Secondary"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (mEvent.event.event_id) {
triggerReply(mEvent.event.event_id, false);
requestClose();
}
}}
/>
<MenuItem
size="300"
after={<Icon size="100" src={Icons.ThreadReply} />}
radii="300"
fill="None"
variant="Secondary"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (mEvent.event.event_id) {
triggerReply(mEvent.event.event_id, true);
requestClose();
}
}}
/>
{canDelete && (
<MenuItem
size="300"
after={<Icon size="100" src={Icons.Delete} />}
radii="300"
fill="None"
variant="Critical"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setModal({
type: ModalType.Delete,
room,
mEvent,
});
}}
/>
)}
</Menu>
);
}

function EventItem({ mEvent, EventContent }: { mEvent: MatrixEvent; EventContent: IContent }) {
const [isHovered, setIsHovered] = useState(false);
return (
<Box
style={{ position: 'relative', width: '100%' }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={() => setIsHovered(!isHovered)}
>
<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>
{isHovered && <MenuOptions mEvent={mEvent} />}
</Box>
);
}

return (
<Box
className={classNames(css.EventHistory, className)}
Expand Down Expand Up @@ -128,25 +268,7 @@ export const EventHistory = as<'div', EventHistoryProps>(
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>
<EventItem mEvent={mEvent} EventContent={EventContent} />
</>
);
})}
Expand Down
11 changes: 10 additions & 1 deletion src/app/components/message/modals/MessageEditHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ 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 }) {
export function MessageEditHistoryItem({
room,
mEvent,
closeMenu,
}: {
room: Room;
mEvent: MatrixEvent;
closeMenu: () => void;
}) {
const setModal = useSetAtom(modalAtom);

return (
Expand All @@ -18,6 +26,7 @@ export function MessageEditHistoryItem({ room, mEvent }: { room: Room; mEvent: M
onClick={(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
setModal({
type: ModalType.EditHistory,
room,
Expand Down
16 changes: 14 additions & 2 deletions src/app/features/room/message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,13 @@ function MessageInternal(
{!hideReadReceipts && (
<MessageReadReceiptItem room={room} eventId={mEvent.getId() ?? ''} />
)}
{isEdited && <MessageEditHistoryItem room={room} mEvent={mEvent} />}
{isEdited && (
<MessageEditHistoryItem
room={room}
mEvent={mEvent}
closeMenu={closeMenu}
/>
)}
{showDeveloperTools && (
<MessageSourceCodeItem room={room} mEvent={mEvent} />
)}
Expand Down Expand Up @@ -1217,7 +1223,13 @@ export const Event = as<'div', EventProps>(
{!hideReadReceipts && (
<MessageReadReceiptItem room={room} eventId={mEvent.getId() ?? ''} />
)}
{isEdited && <MessageEditHistoryItem room={room} mEvent={mEvent} />}
{isEdited && (
<MessageEditHistoryItem
room={room}
mEvent={mEvent}
closeMenu={closeMenu}
/>
)}
{showDeveloperTools && (
<MessageSourceCodeItem room={room} mEvent={mEvent} />
)}
Expand Down
Loading