A bit more idiomatic than my previous attempt

This commit is contained in:
Gigiaj 2025-06-15 16:03:03 -05:00
parent 3ed8260877
commit 2d66641167

View file

@ -36,6 +36,7 @@ import { MatrixEvent, Room } from 'matrix-js-sdk';
import { Relations } from 'matrix-js-sdk/lib/models/relations'; import { Relations } from 'matrix-js-sdk/lib/models/relations';
import classNames from 'classnames'; import classNames from 'classnames';
import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types';
import { useLongPress } from 'use-long-press';
import { import {
AvatarBase, AvatarBase,
BubbleLayout, BubbleLayout,
@ -79,6 +80,9 @@ import { StateEvent } from '../../../../types/matrix/room';
import { getTagIconSrc, PowerLevelTag } from '../../../hooks/usePowerLevelTags'; import { getTagIconSrc, PowerLevelTag } from '../../../hooks/usePowerLevelTags';
import { PowerIcon } from '../../../components/power'; import { PowerIcon } from '../../../components/power';
import colorMXID from '../../../../util/colorMXID'; import colorMXID from '../../../../util/colorMXID';
import { MessageDropdownMenu, MessageOptionsMenu } from '../MessageOptionsMenu';
import { BottomSheetMenu } from '../MobileClickMenu';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void; export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
@ -714,12 +718,15 @@ export const Message = as<'div', MessageProps>(
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const senderId = mEvent.getSender() ?? ''; const senderId = mEvent.getSender() ?? '';
const [hover, setHover] = useState(false); const { hoverProps, isHovered } = useHover({});
const { hoverProps } = useHover({ onHoverChange: setHover }); const { focusWithinProps, isFocusWithin } = useFocusWithin({});
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
const [menuAnchor, setMenuAnchor] = useState<RectCords>(); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [emojiBoardAnchor, setEmojiBoardAnchor] = useState<RectCords>(); const [emojiBoardAnchor, setEmojiBoardAnchor] = useState<RectCords>();
const screenSize = useScreenSizeContext();
const isMobile = screenSize === ScreenSize.Mobile;
const [isDesktopOptionsActive, setDesktopOptionsActive] = useState(false);
const showDesktopOptions = !isMobile && (isHovered || isFocusWithin || isDesktopOptionsActive);
const [isMobileSheetOpen, setMobileSheetOpen] = useState(false);
const senderDisplayName = const senderDisplayName =
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
const senderAvatarMxc = getMemberAvatarMxc(room, senderId); const senderAvatarMxc = getMemberAvatarMxc(room, senderId);
@ -733,6 +740,18 @@ export const Message = as<'div', MessageProps>(
const usernameColor = legacyUsernameColor ? colorMXID(senderId) : tagColor; const usernameColor = legacyUsernameColor ? colorMXID(senderId) : tagColor;
const longPressBinder = useLongPress(
() => {
if (isMobile) {
setMobileSheetOpen(true);
}
},
{
threshold: 400,
cancelOnMovement: true,
}
);
const headerJSX = !collapse && ( const headerJSX = !collapse && (
<Box <Box
gap="300" gap="300"
@ -760,7 +779,7 @@ export const Message = as<'div', MessageProps>(
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />} {tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box> </Box>
<Box shrink="No" gap="100"> <Box shrink="No" gap="100">
{messageLayout === MessageLayout.Modern && hover && ( {messageLayout === MessageLayout.Modern && isHovered && (
<> <>
<Text as="span" size="T200" priority="300"> <Text as="span" size="T200" priority="300">
{senderId} {senderId}
@ -833,263 +852,80 @@ export const Message = as<'div', MessageProps>(
}); });
}; };
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget;
setMenuAnchor(target.getBoundingClientRect());
};
const closeMenu = () => {
setMenuAnchor(undefined);
};
const handleOpenEmojiBoard: MouseEventHandler<HTMLButtonElement> = (evt) => {
const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget;
setEmojiBoardAnchor(target.getBoundingClientRect());
};
const handleAddReactions: MouseEventHandler<HTMLButtonElement> = () => {
const rect = menuAnchor;
closeMenu();
// open it with timeout because closeMenu
// FocusTrap will return focus from emojiBoard
setTimeout(() => {
setEmojiBoardAnchor(rect);
}, 100);
};
return ( return (
<MessageBase <>
className={classNames(css.MessageBase, className)} <MessageBase
tabIndex={0} className={classNames(css.MessageBase, className)}
space={messageSpacing} tabIndex={0}
collapse={collapse} space={messageSpacing}
highlight={highlight} collapse={collapse}
selected={!!menuAnchor || !!emojiBoardAnchor} highlight={highlight}
{...props} selected={isDesktopOptionsActive || isMobileSheetOpen}
{...hoverProps} {...props}
{...focusWithinProps} {...hoverProps}
ref={ref} {...focusWithinProps}
> {...(isMobile ? longPressBinder() : {})}
{!edit && (hover || !!menuAnchor || !!emojiBoardAnchor) && ( ref={ref}
<div className={css.MessageOptionsBase}> >
<Menu className={css.MessageOptionsBar} variant="SurfaceVariant"> {!edit && (isHovered || !!menuAnchor || !!emojiBoardAnchor) && (
<Box gap="100"> <MessageOptionsMenu
{canSendReaction && ( mEvent={mEvent}
<PopOut room={room}
position="Bottom" mx={mx}
align={emojiBoardAnchor?.width === 0 ? 'Start' : 'End'} relations={relations}
offset={emojiBoardAnchor?.width === 0 ? 0 : undefined} imagePackRooms={imagePackRooms}
anchor={emojiBoardAnchor} canSendReaction={canSendReaction}
content={ canEdit={canEditEvent(mx, mEvent)}
<EmojiBoard canDelete={canDelete}
imagePackRooms={imagePackRooms ?? []} canPinEvent={canPinEvent}
returnFocusOnDeactivate={false} hideReadReceipts={hideReadReceipts}
allowTextCustomEmoji onReactionToggle={onReactionToggle}
onEmojiSelect={(key) => { onReplyClick={onReplyClick}
onReactionToggle(mEvent.getId()!, key); onEditId={onEditId}
setEmojiBoardAnchor(undefined); onActiveStateChange={setDesktopOptionsActive}
}} />
onCustomEmojiSelect={(mxc, shortcode) => { )}
onReactionToggle(mEvent.getId()!, mxc, shortcode); {messageLayout === MessageLayout.Compact && (
setEmojiBoardAnchor(undefined); <CompactLayout before={headerJSX} onContextMenu={handleContextMenu}>
}} {msgContentJSX}
requestClose={() => { </CompactLayout>
setEmojiBoardAnchor(undefined); )}
}} {messageLayout === MessageLayout.Bubble && (
/> <BubbleLayout before={avatarJSX} onContextMenu={handleContextMenu}>
} {headerJSX}
> {msgContentJSX}
<IconButton </BubbleLayout>
onClick={handleOpenEmojiBoard} )}
variant="SurfaceVariant" {messageLayout !== MessageLayout.Compact && messageLayout !== MessageLayout.Bubble && (
size="300" <ModernLayout before={avatarJSX} onContextMenu={handleContextMenu}>
radii="300" {headerJSX}
aria-pressed={!!emojiBoardAnchor} {msgContentJSX}
> </ModernLayout>
<Icon src={Icons.SmilePlus} size="100" /> )}
</IconButton> </MessageBase>
</PopOut>
)} {isMobile && (
<IconButton <BottomSheetMenu onClose={() => setMobileSheetOpen(false)} isOpen={isMobileSheetOpen}>
onClick={onReplyClick} <MessageDropdownMenu
data-event-id={mEvent.getId()} closeMenu={() => setMobileSheetOpen(false)}
variant="SurfaceVariant" mEvent={mEvent}
size="300" eventId={mEvent.getId()}
radii="300" room={room}
> mx={mx}
<Icon src={Icons.ReplyArrow} size="100" /> relations={relations}
</IconButton> canSendReaction={canSendReaction}
{canEditEvent(mx, mEvent) && onEditId && ( canEdit={canEditEvent(mx, mEvent)}
<IconButton canDelete={canDelete || mEvent?.getSender() === mx.getUserId()}
onClick={() => onEditId(mEvent.getId())} canPinEvent={canPinEvent}
variant="SurfaceVariant" hideReadReceipts={hideReadReceipts}
size="300" onReactionToggle={onReactionToggle}
radii="300" onReplyClick={onReplyClick}
> onEditId={onEditId}
<Icon src={Icons.Pencil} size="100" /> handleAddReactions={null}
</IconButton> />
)} </BottomSheetMenu>
<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: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<Menu>
{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: any) => {
onReplyClick(evt);
closeMenu();
}}
>
<Text
className={css.MessageMenuItemText}
as="span"
size="T300"
truncate
>
Reply
</Text>
</MenuItem>
{canEditEvent(mx, mEvent) && 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>
</FocusTrap>
}
>
<IconButton
variant="SurfaceVariant"
size="300"
radii="300"
onClick={handleOpenMenu}
aria-pressed={!!menuAnchor}
>
<Icon src={Icons.VerticalDots} size="100" />
</IconButton>
</PopOut>
</Box>
</Menu>
</div>
)} )}
{messageLayout === MessageLayout.Compact && ( </>
<CompactLayout before={headerJSX} onContextMenu={handleContextMenu}>
{msgContentJSX}
</CompactLayout>
)}
{messageLayout === MessageLayout.Bubble && (
<BubbleLayout before={avatarJSX} onContextMenu={handleContextMenu}>
{headerJSX}
{msgContentJSX}
</BubbleLayout>
)}
{messageLayout !== MessageLayout.Compact && messageLayout !== MessageLayout.Bubble && (
<ModernLayout before={avatarJSX} onContextMenu={handleContextMenu}>
{headerJSX}
{msgContentJSX}
</ModernLayout>
)}
</MessageBase>
); );
} }
); );
@ -1155,7 +991,7 @@ export const Event = as<'div', EventProps>(
highlight={highlight} highlight={highlight}
selected={!!menuAnchor} selected={!!menuAnchor}
{...props} {...props}
{...hoverProps} {...hoverProps} // Impacts hover
{...focusWithinProps} {...focusWithinProps}
ref={ref} ref={ref}
> >