mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-13 02:30:29 +03:00
Clean up and pass args forward through parent obj and use shared value destructuring to keep the mobile context menu handling contained in the modularized component
This commit is contained in:
parent
8ae0b75abd
commit
9b97d02c21
1 changed files with 356 additions and 0 deletions
356
src/app/features/room/message/MessageOptionsMenu.tsx
Normal file
356
src/app/features/room/message/MessageOptionsMenu.tsx
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Header,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Icons,
|
||||||
|
Line,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
PopOut,
|
||||||
|
RectCords,
|
||||||
|
Text,
|
||||||
|
} from 'folds';
|
||||||
|
import React, { forwardRef, MouseEventHandler, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { MatrixClient, MatrixEvent, Relations, Room } from 'matrix-js-sdk';
|
||||||
|
import { EmojiBoard } from '../../../components/emoji-board';
|
||||||
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MessageAllReactionItem,
|
||||||
|
MessageCopyLinkItem,
|
||||||
|
MessageDeleteItem,
|
||||||
|
MessagePinItem,
|
||||||
|
MessageQuickReactions,
|
||||||
|
MessageReadReceiptItem,
|
||||||
|
MessageReportItem,
|
||||||
|
MessageSourceCodeItem,
|
||||||
|
} from './Message';
|
||||||
|
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||||
|
import { BottomSheetMenu } from './MobileContextMenu';
|
||||||
|
|
||||||
|
type BaseOptionProps = {
|
||||||
|
mEvent: MatrixEvent;
|
||||||
|
room: Room;
|
||||||
|
mx: MatrixClient;
|
||||||
|
relations: Relations | undefined;
|
||||||
|
canSendReaction: boolean | undefined;
|
||||||
|
canEdit: boolean | undefined;
|
||||||
|
canDelete: boolean | undefined;
|
||||||
|
canPinEvent: boolean | undefined;
|
||||||
|
hideReadReceipts: boolean | undefined;
|
||||||
|
onReactionToggle: (targetEventId: string, key: string, shortcode?: string | undefined) => void;
|
||||||
|
onReplyClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
onEditId: ((eventId?: string | undefined) => void) | undefined;
|
||||||
|
handleAddReactions: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
closeMenu: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MessageDropdownMenu = forwardRef<HTMLDivElement, BaseOptionProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
mEvent,
|
||||||
|
room,
|
||||||
|
mx,
|
||||||
|
relations,
|
||||||
|
canSendReaction,
|
||||||
|
canEdit,
|
||||||
|
canDelete,
|
||||||
|
canPinEvent,
|
||||||
|
hideReadReceipts,
|
||||||
|
onReactionToggle,
|
||||||
|
onReplyClick,
|
||||||
|
onEditId,
|
||||||
|
handleAddReactions,
|
||||||
|
closeMenu,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<Menu ref={ref}>
|
||||||
|
{canSendReaction && (
|
||||||
|
<MessageQuickReactions
|
||||||
|
onReaction={(key, shortcode) => {
|
||||||
|
onReactionToggle(mEvent.getId() ?? '', key, shortcode);
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
|
||||||
|
{canSendReaction && (
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
after={<Icon size="100" src={Icons.SmilePlus} />}
|
||||||
|
radii="300"
|
||||||
|
onClick={handleAddReactions}
|
||||||
|
>
|
||||||
|
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
|
||||||
|
Add Reaction
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{relations && (
|
||||||
|
<MessageAllReactionItem room={room} relations={relations} onClose={closeMenu} />
|
||||||
|
)}
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
after={<Icon size="100" src={Icons.ReplyArrow} />}
|
||||||
|
radii="300"
|
||||||
|
data-event-id={mEvent.getId()}
|
||||||
|
onClick={(evt) => {
|
||||||
|
onReplyClick(evt);
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
|
||||||
|
Reply
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
{canEdit && onEditId && (
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
after={<Icon size="100" src={Icons.Pencil} />}
|
||||||
|
radii="300"
|
||||||
|
data-event-id={mEvent.getId()}
|
||||||
|
onClick={() => {
|
||||||
|
onEditId(mEvent.getId());
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
|
||||||
|
Edit Message
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{!hideReadReceipts && (
|
||||||
|
<MessageReadReceiptItem room={room} eventId={mEvent.getId() ?? ''} onClose={closeMenu} />
|
||||||
|
)}
|
||||||
|
<MessageSourceCodeItem room={room} mEvent={mEvent} onClose={closeMenu} />
|
||||||
|
<MessageCopyLinkItem room={room} mEvent={mEvent} onClose={closeMenu} />
|
||||||
|
{canPinEvent && <MessagePinItem room={room} mEvent={mEvent} onClose={closeMenu} />}
|
||||||
|
</Box>
|
||||||
|
{((!mEvent.isRedacted() && canDelete) || mEvent.getSender() !== mx.getUserId()) && (
|
||||||
|
<>
|
||||||
|
<Line size="300" />
|
||||||
|
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
|
||||||
|
{!mEvent.isRedacted() && canDelete && (
|
||||||
|
<MessageDeleteItem room={room} mEvent={mEvent} onClose={closeMenu} />
|
||||||
|
)}
|
||||||
|
{mEvent.getSender() !== mx.getUserId() && (
|
||||||
|
<MessageReportItem room={room} mEvent={mEvent} onClose={closeMenu} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
type ExtendedOptionsProps = BaseOptionProps & {
|
||||||
|
imagePackRooms: Room[] | undefined;
|
||||||
|
onActiveStateChange: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
menuAnchor: RectCords | undefined;
|
||||||
|
emojiBoardAnchor: RectCords | undefined;
|
||||||
|
handleOpenEmojiBoard: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
handleOpenMenu: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
setMenuAnchor: React.Dispatch<React.SetStateAction<RectCords | undefined>>;
|
||||||
|
setEmojiBoardAnchor: React.Dispatch<React.SetStateAction<RectCords | undefined>>;
|
||||||
|
isMobileSheetOpen;
|
||||||
|
setMobileSheetOpen;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MessageOptionsMenu({
|
||||||
|
mEvent,
|
||||||
|
room,
|
||||||
|
mx,
|
||||||
|
relations,
|
||||||
|
imagePackRooms,
|
||||||
|
canSendReaction,
|
||||||
|
canEdit,
|
||||||
|
canDelete,
|
||||||
|
canPinEvent,
|
||||||
|
hideReadReceipts,
|
||||||
|
onReactionToggle,
|
||||||
|
onReplyClick,
|
||||||
|
onEditId,
|
||||||
|
onActiveStateChange,
|
||||||
|
closeMenu,
|
||||||
|
menuAnchor,
|
||||||
|
emojiBoardAnchor,
|
||||||
|
handleOpenEmojiBoard,
|
||||||
|
handleOpenMenu,
|
||||||
|
handleAddReactions,
|
||||||
|
setMenuAnchor,
|
||||||
|
setEmojiBoardAnchor,
|
||||||
|
isMobileSheetOpen,
|
||||||
|
setMobileSheetOpen,
|
||||||
|
}: ExtendedOptionsProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
onActiveStateChange?.(!!menuAnchor || !!emojiBoardAnchor);
|
||||||
|
}, [emojiBoardAnchor, menuAnchor, onActiveStateChange]);
|
||||||
|
|
||||||
|
const screenSize = useScreenSizeContext();
|
||||||
|
const isMobile = screenSize === ScreenSize.Mobile;
|
||||||
|
const [view, setView] = useState('options');
|
||||||
|
|
||||||
|
const eventId = mEvent.getId();
|
||||||
|
if (!eventId) return null;
|
||||||
|
|
||||||
|
const optionProps: BaseOptionProps = {
|
||||||
|
mEvent,
|
||||||
|
room,
|
||||||
|
mx,
|
||||||
|
relations,
|
||||||
|
canSendReaction,
|
||||||
|
canEdit,
|
||||||
|
canDelete,
|
||||||
|
canPinEvent,
|
||||||
|
hideReadReceipts,
|
||||||
|
onReactionToggle,
|
||||||
|
onReplyClick,
|
||||||
|
onEditId,
|
||||||
|
handleAddReactions,
|
||||||
|
closeMenu,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<BottomSheetMenu onClose={() => setMobileSheetOpen(false)} isOpen={isMobileSheetOpen}>
|
||||||
|
{view === 'options' ? (
|
||||||
|
<MessageDropdownMenu
|
||||||
|
{...optionProps}
|
||||||
|
closeMenu={() => {
|
||||||
|
closeMenu();
|
||||||
|
setMobileSheetOpen(false);
|
||||||
|
}}
|
||||||
|
handleAddReactions={() => setView('emoji')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box direction="Column" style={{ width: '100%' }}>
|
||||||
|
<Header variant="Surface" size="500">
|
||||||
|
<IconButton size="300" onClick={() => setView('options')}>
|
||||||
|
<Icon src={Icons.ArrowLeft} />
|
||||||
|
</IconButton>
|
||||||
|
<Box grow="Yes" alignItems="Center">
|
||||||
|
<Text size="H4">Add Reaction</Text>
|
||||||
|
</Box>
|
||||||
|
</Header>
|
||||||
|
<EmojiBoard
|
||||||
|
imagePackRooms={imagePackRooms ?? []}
|
||||||
|
returnFocusOnDeactivate={false}
|
||||||
|
allowTextCustomEmoji
|
||||||
|
onEmojiSelect={(key) => {
|
||||||
|
onReactionToggle(mEvent.getId(), key);
|
||||||
|
setEmojiBoardAnchor(undefined);
|
||||||
|
closeMenu();
|
||||||
|
setMobileSheetOpen(false);
|
||||||
|
}}
|
||||||
|
onCustomEmojiSelect={(mxc, shortcode) => {
|
||||||
|
onReactionToggle(mEvent.getId(), mxc, shortcode);
|
||||||
|
setEmojiBoardAnchor(undefined);
|
||||||
|
closeMenu();
|
||||||
|
setMobileSheetOpen(false);
|
||||||
|
}}
|
||||||
|
requestClose={() => setEmojiBoardAnchor(undefined)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</BottomSheetMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={css.MessageOptionsBase}>
|
||||||
|
<Menu className={css.MessageOptionsBar} variant="SurfaceVariant">
|
||||||
|
<Box gap="100">
|
||||||
|
{canSendReaction && (
|
||||||
|
<PopOut
|
||||||
|
position="Bottom"
|
||||||
|
align={emojiBoardAnchor?.width === 0 ? 'Start' : 'End'}
|
||||||
|
offset={emojiBoardAnchor?.width === 0 ? 0 : undefined}
|
||||||
|
anchor={emojiBoardAnchor}
|
||||||
|
content={
|
||||||
|
<EmojiBoard
|
||||||
|
imagePackRooms={imagePackRooms ?? []}
|
||||||
|
returnFocusOnDeactivate={false}
|
||||||
|
allowTextCustomEmoji
|
||||||
|
onEmojiSelect={(key) => {
|
||||||
|
onReactionToggle(eventId, key);
|
||||||
|
setEmojiBoardAnchor(undefined);
|
||||||
|
}}
|
||||||
|
onCustomEmojiSelect={(mxc, shortcode) => {
|
||||||
|
onReactionToggle(eventId, mxc, shortcode);
|
||||||
|
setEmojiBoardAnchor(undefined);
|
||||||
|
}}
|
||||||
|
requestClose={() => setEmojiBoardAnchor(undefined)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleOpenEmojiBoard}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
aria-pressed={!!emojiBoardAnchor}
|
||||||
|
>
|
||||||
|
<Icon src={Icons.SmilePlus} size="100" />
|
||||||
|
</IconButton>
|
||||||
|
</PopOut>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
onClick={onReplyClick}
|
||||||
|
data-event-id={eventId}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Icon src={Icons.ReplyArrow} size="100" />
|
||||||
|
</IconButton>
|
||||||
|
{canEdit && onEditId && (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => onEditId(eventId)}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Icon src={Icons.Pencil} size="100" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<PopOut
|
||||||
|
anchor={menuAnchor}
|
||||||
|
position="Bottom"
|
||||||
|
align={menuAnchor?.width === 0 ? 'Start' : 'End'}
|
||||||
|
offset={menuAnchor?.width === 0 ? 0 : undefined}
|
||||||
|
content={
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setMenuAnchor(undefined),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
isKeyForward: (evt) => evt.key === 'ArrowDown',
|
||||||
|
isKeyBackward: (evt) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MessageDropdownMenu {...optionProps} />
|
||||||
|
</FocusTrap>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
aria-pressed={!!menuAnchor}
|
||||||
|
>
|
||||||
|
<Icon src={Icons.VerticalDots} size="100" />
|
||||||
|
</IconButton>
|
||||||
|
</PopOut>
|
||||||
|
</Box>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue