mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-05 15:00:30 +03:00
A bit more idiomatic than my previous attempt
This commit is contained in:
parent
3ed8260877
commit
2d66641167
1 changed files with 97 additions and 261 deletions
|
|
@ -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,245 +852,39 @@ 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
|
<MessageBase
|
||||||
className={classNames(css.MessageBase, className)}
|
className={classNames(css.MessageBase, className)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
space={messageSpacing}
|
space={messageSpacing}
|
||||||
collapse={collapse}
|
collapse={collapse}
|
||||||
highlight={highlight}
|
highlight={highlight}
|
||||||
selected={!!menuAnchor || !!emojiBoardAnchor}
|
selected={isDesktopOptionsActive || isMobileSheetOpen}
|
||||||
{...props}
|
{...props}
|
||||||
{...hoverProps}
|
{...hoverProps}
|
||||||
{...focusWithinProps}
|
{...focusWithinProps}
|
||||||
|
{...(isMobile ? longPressBinder() : {})}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
{!edit && (hover || !!menuAnchor || !!emojiBoardAnchor) && (
|
{!edit && (isHovered || !!menuAnchor || !!emojiBoardAnchor) && (
|
||||||
<div className={css.MessageOptionsBase}>
|
<MessageOptionsMenu
|
||||||
<Menu className={css.MessageOptionsBar} variant="SurfaceVariant">
|
mEvent={mEvent}
|
||||||
<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(mEvent.getId()!, key);
|
|
||||||
setEmojiBoardAnchor(undefined);
|
|
||||||
}}
|
|
||||||
onCustomEmojiSelect={(mxc, shortcode) => {
|
|
||||||
onReactionToggle(mEvent.getId()!, 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={mEvent.getId()}
|
|
||||||
variant="SurfaceVariant"
|
|
||||||
size="300"
|
|
||||||
radii="300"
|
|
||||||
>
|
|
||||||
<Icon src={Icons.ReplyArrow} size="100" />
|
|
||||||
</IconButton>
|
|
||||||
{canEditEvent(mx, mEvent) && onEditId && (
|
|
||||||
<IconButton
|
|
||||||
onClick={() => onEditId(mEvent.getId())}
|
|
||||||
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: 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}
|
room={room}
|
||||||
|
mx={mx}
|
||||||
relations={relations}
|
relations={relations}
|
||||||
onClose={closeMenu}
|
imagePackRooms={imagePackRooms}
|
||||||
|
canSendReaction={canSendReaction}
|
||||||
|
canEdit={canEditEvent(mx, mEvent)}
|
||||||
|
canDelete={canDelete}
|
||||||
|
canPinEvent={canPinEvent}
|
||||||
|
hideReadReceipts={hideReadReceipts}
|
||||||
|
onReactionToggle={onReactionToggle}
|
||||||
|
onReplyClick={onReplyClick}
|
||||||
|
onEditId={onEditId}
|
||||||
|
onActiveStateChange={setDesktopOptionsActive}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<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 && (
|
{messageLayout === MessageLayout.Compact && (
|
||||||
<CompactLayout before={headerJSX} onContextMenu={handleContextMenu}>
|
<CompactLayout before={headerJSX} onContextMenu={handleContextMenu}>
|
||||||
{msgContentJSX}
|
{msgContentJSX}
|
||||||
|
|
@ -1090,6 +903,29 @@ export const Message = as<'div', MessageProps>(
|
||||||
</ModernLayout>
|
</ModernLayout>
|
||||||
)}
|
)}
|
||||||
</MessageBase>
|
</MessageBase>
|
||||||
|
|
||||||
|
{isMobile && (
|
||||||
|
<BottomSheetMenu onClose={() => setMobileSheetOpen(false)} isOpen={isMobileSheetOpen}>
|
||||||
|
<MessageDropdownMenu
|
||||||
|
closeMenu={() => setMobileSheetOpen(false)}
|
||||||
|
mEvent={mEvent}
|
||||||
|
eventId={mEvent.getId()}
|
||||||
|
room={room}
|
||||||
|
mx={mx}
|
||||||
|
relations={relations}
|
||||||
|
canSendReaction={canSendReaction}
|
||||||
|
canEdit={canEditEvent(mx, mEvent)}
|
||||||
|
canDelete={canDelete || mEvent?.getSender() === mx.getUserId()}
|
||||||
|
canPinEvent={canPinEvent}
|
||||||
|
hideReadReceipts={hideReadReceipts}
|
||||||
|
onReactionToggle={onReactionToggle}
|
||||||
|
onReplyClick={onReplyClick}
|
||||||
|
onEditId={onEditId}
|
||||||
|
handleAddReactions={null}
|
||||||
|
/>
|
||||||
|
</BottomSheetMenu>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue