From 2d66641167b32a211fe5bb127d3ca228a8da379f Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 16:03:03 -0500 Subject: [PATCH 01/73] A bit more idiomatic than my previous attempt --- src/app/features/room/message/Message.tsx | 358 ++++++---------------- 1 file changed, 97 insertions(+), 261 deletions(-) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index ae971ab8..39d172db 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -36,6 +36,7 @@ import { MatrixEvent, Room } from 'matrix-js-sdk'; import { Relations } from 'matrix-js-sdk/lib/models/relations'; import classNames from 'classnames'; import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types'; +import { useLongPress } from 'use-long-press'; import { AvatarBase, BubbleLayout, @@ -79,6 +80,9 @@ import { StateEvent } from '../../../../types/matrix/room'; import { getTagIconSrc, PowerLevelTag } from '../../../hooks/usePowerLevelTags'; import { PowerIcon } from '../../../components/power'; 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; @@ -714,12 +718,15 @@ export const Message = as<'div', MessageProps>( const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const senderId = mEvent.getSender() ?? ''; - const [hover, setHover] = useState(false); - const { hoverProps } = useHover({ onHoverChange: setHover }); - const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); + const { hoverProps, isHovered } = useHover({}); + const { focusWithinProps, isFocusWithin } = useFocusWithin({}); const [menuAnchor, setMenuAnchor] = useState(); const [emojiBoardAnchor, setEmojiBoardAnchor] = useState(); - + 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 = getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; const senderAvatarMxc = getMemberAvatarMxc(room, senderId); @@ -733,6 +740,18 @@ export const Message = as<'div', MessageProps>( const usernameColor = legacyUsernameColor ? colorMXID(senderId) : tagColor; + const longPressBinder = useLongPress( + () => { + if (isMobile) { + setMobileSheetOpen(true); + } + }, + { + threshold: 400, + cancelOnMovement: true, + } + ); + const headerJSX = !collapse && ( ( {tagIconSrc && } - {messageLayout === MessageLayout.Modern && hover && ( + {messageLayout === MessageLayout.Modern && isHovered && ( <> {senderId} @@ -833,263 +852,80 @@ export const Message = as<'div', MessageProps>( }); }; - const handleOpenMenu: MouseEventHandler = (evt) => { - const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget; - setMenuAnchor(target.getBoundingClientRect()); - }; - - const closeMenu = () => { - setMenuAnchor(undefined); - }; - - const handleOpenEmojiBoard: MouseEventHandler = (evt) => { - const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget; - setEmojiBoardAnchor(target.getBoundingClientRect()); - }; - const handleAddReactions: MouseEventHandler = () => { - const rect = menuAnchor; - closeMenu(); - // open it with timeout because closeMenu - // FocusTrap will return focus from emojiBoard - - setTimeout(() => { - setEmojiBoardAnchor(rect); - }, 100); - }; - return ( - - {!edit && (hover || !!menuAnchor || !!emojiBoardAnchor) && ( -
- - - {canSendReaction && ( - { - onReactionToggle(mEvent.getId()!, key); - setEmojiBoardAnchor(undefined); - }} - onCustomEmojiSelect={(mxc, shortcode) => { - onReactionToggle(mEvent.getId()!, mxc, shortcode); - setEmojiBoardAnchor(undefined); - }} - requestClose={() => { - setEmojiBoardAnchor(undefined); - }} - /> - } - > - - - - - )} - - - - {canEditEvent(mx, mEvent) && onEditId && ( - onEditId(mEvent.getId())} - variant="SurfaceVariant" - size="300" - radii="300" - > - - - )} - setMenuAnchor(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', - isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', - escapeDeactivates: stopPropagation, - }} - > - - {canSendReaction && ( - { - onReactionToggle(mEvent.getId()!, key, shortcode); - closeMenu(); - }} - /> - )} - - {canSendReaction && ( - } - radii="300" - onClick={handleAddReactions} - > - - Add Reaction - - - )} - {relations && ( - - )} - } - radii="300" - data-event-id={mEvent.getId()} - onClick={(evt: any) => { - onReplyClick(evt); - closeMenu(); - }} - > - - Reply - - - {canEditEvent(mx, mEvent) && onEditId && ( - } - radii="300" - data-event-id={mEvent.getId()} - onClick={() => { - onEditId(mEvent.getId()); - closeMenu(); - }} - > - - Edit Message - - - )} - {!hideReadReceipts && ( - - )} - - - {canPinEvent && ( - - )} - - {((!mEvent.isRedacted() && canDelete) || - mEvent.getSender() !== mx.getUserId()) && ( - <> - - - {!mEvent.isRedacted() && canDelete && ( - - )} - {mEvent.getSender() !== mx.getUserId() && ( - - )} - - - )} - - - } - > - - - - - - -
+ <> + + {!edit && (isHovered || !!menuAnchor || !!emojiBoardAnchor) && ( + + )} + {messageLayout === MessageLayout.Compact && ( + + {msgContentJSX} + + )} + {messageLayout === MessageLayout.Bubble && ( + + {headerJSX} + {msgContentJSX} + + )} + {messageLayout !== MessageLayout.Compact && messageLayout !== MessageLayout.Bubble && ( + + {headerJSX} + {msgContentJSX} + + )} + + + {isMobile && ( + setMobileSheetOpen(false)} isOpen={isMobileSheetOpen}> + 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} + /> + )} - {messageLayout === MessageLayout.Compact && ( - - {msgContentJSX} - - )} - {messageLayout === MessageLayout.Bubble && ( - - {headerJSX} - {msgContentJSX} - - )} - {messageLayout !== MessageLayout.Compact && messageLayout !== MessageLayout.Bubble && ( - - {headerJSX} - {msgContentJSX} - - )} -
+ ); } ); @@ -1155,7 +991,7 @@ export const Event = as<'div', EventProps>( highlight={highlight} selected={!!menuAnchor} {...props} - {...hoverProps} + {...hoverProps} // Impacts hover {...focusWithinProps} ref={ref} > From 5d3033aa96e20f5441cbc0d890b7a8fe9b4d56e5 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 16:04:56 -0500 Subject: [PATCH 02/73] move t and handleEdit --- src/app/features/room/RoomTimeline.tsx | 44 ++++++++------------------ 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 05caf4b0..7b58ee6b 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -435,6 +435,7 @@ export function RoomTimeline({ accessibleTagColors, }: RoomTimelineProps) { const mx = useMatrixClient(); + const { t } = useTranslation(); const useAuthentication = useMediaAuthentication(); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [messageLayout] = useSetting(settingsAtom, 'messageLayout'); @@ -592,15 +593,8 @@ export function RoomTimeline({ room, useCallback( (mEvt: MatrixEvent) => { - // if user is at bottom of timeline - // keep paginating timeline and conditionally mark as read - // otherwise we update timeline without paginating - // so timeline can be updated with evt like: edits, reactions etc if (atBottomRef.current) { if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) { - // Check if the document is in focus (user is actively viewing the app), - // and either there are no unread messages or the latest message is from the current user. - // If either condition is met, trigger the markAsRead function to send a read receipt. requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!, hideActivity)); } @@ -668,13 +662,11 @@ export function RoomTimeline({ }, [room, liveTimelineLinked]) ); - // Stay at bottom when room editor resize useResizeObserver( useMemo(() => { let mounted = false; return (entries) => { if (!mounted) { - // skip initial mounting call mounted = true; return; } @@ -742,8 +734,6 @@ export function RoomTimeline({ if (inFocus && atBottomRef.current) { if (unreadInfo?.inLiveTimeline) { handleOpenEvent(unreadInfo.readUptoEventId, false, (scrolled) => { - // the unread event is already in view - // so, try mark as read; if (!scrolled) { tryAutoMarkAsRead(); } @@ -757,7 +747,6 @@ export function RoomTimeline({ ) ); - // Handle up arrow edit useKeyDown( window, useCallback( @@ -788,7 +777,6 @@ export function RoomTimeline({ } }, [eventId, loadEventTimeline]); - // Scroll to bottom on initial timeline load useLayoutEffect(() => { const scrollEl = scrollRef.current; if (scrollEl) { @@ -796,8 +784,6 @@ export function RoomTimeline({ } }, []); - // if live timeline is linked and unreadInfo change - // Scroll to last read message useLayoutEffect(() => { const { readUptoEventId, inLiveTimeline, scrollTo } = unreadInfo ?? {}; if (readUptoEventId && inLiveTimeline && scrollTo) { @@ -815,7 +801,6 @@ export function RoomTimeline({ } }, [room, unreadInfo, scrollToItem]); - // scroll to focused message useLayoutEffect(() => { if (focusItem && focusItem.scrollTo) { scrollToItem(focusItem.index, { @@ -834,7 +819,6 @@ export function RoomTimeline({ }, 2000); }, [alive, focusItem, scrollToItem]); - // scroll to bottom of timeline const scrollToBottomCount = scrollToBottomRef.current.count; useLayoutEffect(() => { if (scrollToBottomCount > 0) { @@ -844,14 +828,24 @@ export function RoomTimeline({ } }, [scrollToBottomCount]); - // Remove unreadInfo on mark as read useEffect(() => { if (!unread) { setUnreadInfo(undefined); } }, [unread]); - // scroll out of view msg editor in view. + const handleEdit = useCallback( + (editEvtId?: string) => { + if (editEvtId) { + setEditId(editEvtId); + return; + } + setEditId(undefined); + ReactEditor.focus(editor); + }, + [editor] + ); + useEffect(() => { if (editId) { const editMsgElement = @@ -982,18 +976,6 @@ export function RoomTimeline({ }, [mx, room] ); - const handleEdit = useCallback( - (editEvtId?: string) => { - if (editEvtId) { - setEditId(editEvtId); - return; - } - setEditId(undefined); - ReactEditor.focus(editor); - }, - [editor] - ); - const { t } = useTranslation(); const renderMatrixEvent = useMatrixEventRenderer< [string, MatrixEvent, number, EventTimelineSet, boolean] From e2bbc429149b4bc027788975a3dc97c1311c6eea Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 16:05:33 -0500 Subject: [PATCH 03/73] Pull out menu options and the dropdown into their own components for reuse --- src/app/features/room/MessageOptionsMenu.tsx | 273 +++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 src/app/features/room/MessageOptionsMenu.tsx diff --git a/src/app/features/room/MessageOptionsMenu.tsx b/src/app/features/room/MessageOptionsMenu.tsx new file mode 100644 index 00000000..43e5b751 --- /dev/null +++ b/src/app/features/room/MessageOptionsMenu.tsx @@ -0,0 +1,273 @@ +import { Box, Icon, IconButton, Icons, Line, Menu, MenuItem, PopOut, RectCords, Text } from 'folds'; +import React, { MouseEventHandler, useEffect, useState } from 'react'; + +import FocusTrap from 'focus-trap-react'; +import { EmojiBoard } from '../../components/emoji-board'; +import { stopPropagation } from '../../utils/keyboard'; +import * as css from './message/styles.css'; + +import { + MessageAllReactionItem, + MessageCopyLinkItem, + MessageDeleteItem, + MessagePinItem, + MessageQuickReactions, + MessageReadReceiptItem, + MessageReportItem, + MessageSourceCodeItem, +} from './message/Message'; + +export function MessageDropdownMenu({ + mEvent, + room, + mx, + relations, + eventId, + canSendReaction, + canEdit, + canDelete, + canPinEvent, + hideReadReceipts, + onReactionToggle, + onReplyClick, + onEditId, + handleAddReactions, + closeMenu, +}) { + return ( + + {canSendReaction && ( + { + onReactionToggle(eventId, key, shortcode); + closeMenu(); + }} + /> + )} + + {canSendReaction && ( + } + radii="300" + onClick={handleAddReactions} + > + + Add Reaction + + + )} + {relations && ( + + )} + } + radii="300" + data-event-id={eventId} + onClick={(evt) => { + onReplyClick(evt); + closeMenu(); + }} + > + + Reply + + + {canEdit && onEditId && ( + } + radii="300" + data-event-id={eventId} + onClick={() => { + onEditId(eventId); + closeMenu(); + }} + > + + Edit Message + + + )} + {!hideReadReceipts && ( + + )} + + + {canPinEvent && } + + {/* Redact and Report actions */} + {((!mEvent.isRedacted() && canDelete) || mEvent.getSender() !== mx.getUserId()) && ( + <> + + + {!mEvent.isRedacted() && canDelete && ( + + )} + {mEvent.getSender() !== mx.getUserId() && ( + + )} + + + )} + + ); +} + +export function MessageOptionsMenu({ + mEvent, + room, + mx, + relations, + imagePackRooms, + canSendReaction, + canEdit, + canDelete, + canPinEvent, + hideReadReceipts, + onReactionToggle, + onReplyClick, + onEditId, + onActiveStateChange, +}) { + const [menuAnchor, setMenuAnchor] = useState(); + const [emojiBoardAnchor, setEmojiBoardAnchor] = useState(); + + useEffect(() => { + onActiveStateChange?.(!!menuAnchor || !!emojiBoardAnchor); + }, [emojiBoardAnchor, menuAnchor, onActiveStateChange]); + + const eventId = mEvent.getId(); + if (!eventId) return null; + + const closeMenu = () => { + setMenuAnchor(undefined); + }; + + const handleOpenMenu: MouseEventHandler = (evt) => { + const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget; + setMenuAnchor(target.getBoundingClientRect()); + }; + + const handleOpenEmojiBoard: MouseEventHandler = (evt) => { + const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget; + setEmojiBoardAnchor(target.getBoundingClientRect()); + }; + + const handleAddReactions: MouseEventHandler = () => { + const rect = menuAnchor; + closeMenu(); + // Use a timeout to allow the first menu to close before opening the next + setTimeout(() => { + setEmojiBoardAnchor(rect); + }, 100); + }; + return ( +
+ + + {canSendReaction && ( + { + onReactionToggle(eventId, key); + setEmojiBoardAnchor(undefined); + }} + onCustomEmojiSelect={(mxc, shortcode) => { + onReactionToggle(eventId, mxc, shortcode); + setEmojiBoardAnchor(undefined); + }} + requestClose={() => setEmojiBoardAnchor(undefined)} + /> + } + > + + + + + )} + + + + {canEdit && onEditId && ( + onEditId(eventId)} + variant="SurfaceVariant" + size="300" + radii="300" + > + + + )} + setMenuAnchor(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt) => evt.key === 'ArrowDown', + isKeyBackward: (evt) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, + }} + > + + + } + > + + + + + + +
+ ); +} From 41538561ee6039b02d89f90e1dca48bcc5772c83 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 16:06:15 -0500 Subject: [PATCH 04/73] Add a placeholder class for now (move to MessageOptionsMenu later) --- src/app/features/room/MobileClickMenu.tsx | 61 +++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/app/features/room/MobileClickMenu.tsx diff --git a/src/app/features/room/MobileClickMenu.tsx b/src/app/features/room/MobileClickMenu.tsx new file mode 100644 index 00000000..48489079 --- /dev/null +++ b/src/app/features/room/MobileClickMenu.tsx @@ -0,0 +1,61 @@ +/* eslint-disable react/destructuring-assignment */ +import React from 'react'; +import { Box, Icon, Icons, Text } from 'folds'; +import classNames from 'classnames'; +import * as css from './RoomTimeline.css'; + +export function BottomSheetMenu({ + isOpen, + onClose, + children, +}: { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; +}) { + return ( +
+ + ); +} + +export function MenuItemButton({ + icon, + label, + onClick, + destructive = false, +}: { + icon: string; + label: string; + onClick: () => void; + destructive?: boolean; +}) { + return ( + + + + {label} + + + ); +} From 64ea5f262b475b11a836dd90a4f4b76afd5d09b8 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 16:09:01 -0500 Subject: [PATCH 05/73] from earlier attempt to remove highlighting on the POC component (not the current one at all) --- .../components/message/layout/layout.css.ts | 82 ++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/src/app/components/message/layout/layout.css.ts b/src/app/components/message/layout/layout.css.ts index a9b3f35f..a61c7f8d 100644 --- a/src/app/components/message/layout/layout.css.ts +++ b/src/app/components/message/layout/layout.css.ts @@ -174,16 +174,94 @@ export const MessageTextBody = recipe({ jumboEmoji: { true: { fontSize: '1.504em', - lineHeight: '1.4962em', + lineHeight: 1.1, }, }, emote: { true: { - color: color.Success.Main, fontStyle: 'italic', }, }, }, + + '@media': { + 'screen and (max-width: 768px)': { + base: { + userSelect: 'none', + WebkitUserSelect: 'none', + MozUserSelect: 'none', + msUserSelect: 'none', + }, + }, + }, }); export type MessageTextBodyVariants = RecipeVariants; + +export const menuBackdrop = style({ + userSelect: 'none', + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + transition: 'opacity 0.3s ease-in-out', + opacity: 0, +}); + +export const menuBackdropOpen = style({ + opacity: 1, +}); + +export const menuSheet = style({ + userSelect: 'none', + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + backgroundColor: color.Background.Container, + borderTopLeftRadius: config.radii.R400, + borderTopRightRadius: config.radii.R400, + padding: config.space.S500, + transform: 'translateY(100%)', + transition: 'transform 0.3s ease-out', + boxShadow: '0 -2px 10px rgba(0,0,0,0.1)', +}); + +export const menuSheetOpen = style({ + transform: 'translateY(0)', +}); + +export const menuItem = style({ + userSelect: 'none', + width: '100%', + background: 'none', + border: 'none', + padding: `${config.space.S300} ${config.space.S100}`, + textAlign: 'left', + cursor: 'pointer', + borderRadius: config.radii.R300, + color: color.Primary.ContainerActive, + + outline: 'none', + + WebkitTapHighlightColor: 'transparent', + + WebkitUserSelect: 'none', + MozUserSelect: 'none', + msUserSelect: 'none', + + selectors: { + '&:hover': { + backgroundColor: color.Background.ContainerHover, + }, + '&:focus-visible': { + backgroundColor: color.Background.ContainerHover, + }, + }, +}); + +export const menuItemDestructive = style({ + color: color.Critical.ContainerActive, +}); From d8a51b41e0a7b9baeed466118b7418f9f8a357bd Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 16:09:12 -0500 Subject: [PATCH 06/73] Add use-long-press --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c1cef8c..7622dbc8 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,8 @@ "slate-history": "0.110.3", "slate-react": "0.112.1", "tippy.js": "6.3.7", - "ua-parser-js": "1.0.35" + "ua-parser-js": "1.0.35", + "use-long-press": "3.3.0" }, "devDependencies": { "@esbuild-plugins/node-globals-polyfill": "0.2.3", From df7b71b768c9edd0b5721e9b1e4cddd595b0c7ea Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 16:09:30 -0500 Subject: [PATCH 07/73] update package lock --- package-lock.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 7dd2bf0a..3d2e8d05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,8 @@ "slate-history": "0.110.3", "slate-react": "0.112.1", "tippy.js": "6.3.7", - "ua-parser-js": "1.0.35" + "ua-parser-js": "1.0.35", + "use-long-press": "3.3.0" }, "devDependencies": { "@esbuild-plugins/node-globals-polyfill": "0.2.3", @@ -11311,6 +11312,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-long-press": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/use-long-press/-/use-long-press-3.3.0.tgz", + "integrity": "sha512-Yedz46ILxsb1BTS6kUzpV/wyEZPUlJDq+8Oat0LP1eOZQHbS887baJHJbIGENqCo8wTKNxmoTHLdY8lU/e+wvw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", From 770732beb4e17f95880152f19f0e4748e3e38456 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 20:04:12 -0500 Subject: [PATCH 08/73] Pull out the dupes and just pass them parameter bloat is fine here as it shouldn't grow too much more --- src/app/features/room/MessageOptionsMenu.tsx | 34 ++++++-------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/app/features/room/MessageOptionsMenu.tsx b/src/app/features/room/MessageOptionsMenu.tsx index 43e5b751..52c44cde 100644 --- a/src/app/features/room/MessageOptionsMenu.tsx +++ b/src/app/features/room/MessageOptionsMenu.tsx @@ -130,9 +130,17 @@ export function MessageOptionsMenu({ onReplyClick, onEditId, onActiveStateChange, + closeMenu, + menuAnchor, + emojiBoardAnchor, + handleOpenEmojiBoard, + handleOpenMenu, + handleAddReactions, + setMenuAnchor, + setEmojiBoardAnchor, }) { - const [menuAnchor, setMenuAnchor] = useState(); - const [emojiBoardAnchor, setEmojiBoardAnchor] = useState(); + // const [menuAnchor, setMenuAnchor] = useState(); + // const [emojiBoardAnchor, setEmojiBoardAnchor] = useState(); useEffect(() => { onActiveStateChange?.(!!menuAnchor || !!emojiBoardAnchor); @@ -141,28 +149,6 @@ export function MessageOptionsMenu({ const eventId = mEvent.getId(); if (!eventId) return null; - const closeMenu = () => { - setMenuAnchor(undefined); - }; - - const handleOpenMenu: MouseEventHandler = (evt) => { - const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget; - setMenuAnchor(target.getBoundingClientRect()); - }; - - const handleOpenEmojiBoard: MouseEventHandler = (evt) => { - const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget; - setEmojiBoardAnchor(target.getBoundingClientRect()); - }; - - const handleAddReactions: MouseEventHandler = () => { - const rect = menuAnchor; - closeMenu(); - // Use a timeout to allow the first menu to close before opening the next - setTimeout(() => { - setEmojiBoardAnchor(rect); - }, 100); - }; return (
From d093fc35efa463fb68969664788036932585aeef Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 20:05:20 -0500 Subject: [PATCH 09/73] readd these and pass them --- src/app/features/room/message/Message.tsx | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 39d172db..b999d3a8 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -731,6 +731,28 @@ export const Message = as<'div', MessageProps>( getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; const senderAvatarMxc = getMemberAvatarMxc(room, senderId); + const closeMenu = () => { + setMenuAnchor(undefined); + }; + + const handleOpenMenu: MouseEventHandler = (evt) => { + const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget; + setMenuAnchor(target.getBoundingClientRect()); + }; + + const handleOpenEmojiBoard: MouseEventHandler = (evt) => { + const target = evt.currentTarget.parentElement?.parentElement ?? evt.currentTarget; + setEmojiBoardAnchor(target.getBoundingClientRect()); + }; + + const handleAddReactions: MouseEventHandler = () => { + const rect = menuAnchor; + closeMenu(); + setTimeout(() => { + setEmojiBoardAnchor(rect); + }, 100); + }; + const tagColor = powerLevelTag?.color ? accessibleTagColors?.get(powerLevelTag.color) : undefined; From db0525ff75a275371c018eaa82d22534ef5c1589 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 20:06:03 -0500 Subject: [PATCH 10/73] Add sheet state --- src/app/features/room/message/Message.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index b999d3a8..dc58061e 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -730,6 +730,7 @@ export const Message = as<'div', MessageProps>( const senderDisplayName = getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; const senderAvatarMxc = getMemberAvatarMxc(room, senderId); + const [view, setView] = useState('options'); const closeMenu = () => { setMenuAnchor(undefined); From f2a7a42756570891c81a673c615dd44747d98c23 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 20:06:14 -0500 Subject: [PATCH 11/73] add methods for sheet management --- src/app/features/room/message/Message.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index dc58061e..9aa054ef 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -754,6 +754,16 @@ export const Message = as<'div', MessageProps>( }, 100); }; + const handleClose = () => { + setView('options'); + closeMenu(); + }; + + const onEmojiSelect = (key, shortcode) => { + onReactionToggle(mEvent.getId(), key, shortcode); + handleClose(); + }; + const tagColor = powerLevelTag?.color ? accessibleTagColors?.get(powerLevelTag.color) : undefined; From e3375f62413a70127c8524bd92e8c487d14e5b54 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 20:06:45 -0500 Subject: [PATCH 12/73] pass moved params thru for component --- src/app/features/room/message/Message.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 9aa054ef..0d21f9dc 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -916,6 +916,14 @@ export const Message = as<'div', MessageProps>( onReplyClick={onReplyClick} onEditId={onEditId} onActiveStateChange={setDesktopOptionsActive} + handleAddReactions={handleAddReactions} + closeMenu={closeMenu} + emojiBoardAnchor={emojiBoardAnchor} + menuAnchor={menuAnchor} + handleOpenEmojiBoard={handleOpenEmojiBoard} + setEmojiBoardAnchor={setEmojiBoardAnchor} + setMenuAnchor={setMenuAnchor} + handleOpenMenu={handleOpenMenu} /> )} {messageLayout === MessageLayout.Compact && ( From b25011e0e5b1da23803996464c3f0f4029737508 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 20:07:18 -0500 Subject: [PATCH 13/73] restructure to use Emojiboard here or our MessageDropdownMenu --- src/app/features/room/message/Message.tsx | 68 +++++++++++++++++------ 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 0d21f9dc..148b75aa 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -947,23 +947,57 @@ export const Message = as<'div', MessageProps>( {isMobile && ( setMobileSheetOpen(false)} isOpen={isMobileSheetOpen}> - 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} - /> + {view === 'options' ? ( + { + 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={() => setView('emoji')} + /> + ) : ( + +
+ setView('options')}> + + + + Add Reaction + +
+ { + 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)} + /> +
+ )}
)} From 5b31ce62886a09196872f965cbc2ba775c7f174d Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 22:03:08 -0500 Subject: [PATCH 14/73] Add imports from sheets file to merge in --- src/app/features/room/MessageOptionsMenu.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/features/room/MessageOptionsMenu.tsx b/src/app/features/room/MessageOptionsMenu.tsx index 52c44cde..c6dfe23e 100644 --- a/src/app/features/room/MessageOptionsMenu.tsx +++ b/src/app/features/room/MessageOptionsMenu.tsx @@ -1,7 +1,9 @@ import { Box, Icon, IconButton, Icons, Line, Menu, MenuItem, PopOut, RectCords, Text } from 'folds'; -import React, { MouseEventHandler, useEffect, useState } from 'react'; +import React, { MouseEventHandler, useEffect } 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 './message/styles.css'; From 5466e440a99e1f3d304f12809096dc691f7d73b1 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 22:03:15 -0500 Subject: [PATCH 15/73] Add Base props --- src/app/features/room/MessageOptionsMenu.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/app/features/room/MessageOptionsMenu.tsx b/src/app/features/room/MessageOptionsMenu.tsx index c6dfe23e..9b7fdd2c 100644 --- a/src/app/features/room/MessageOptionsMenu.tsx +++ b/src/app/features/room/MessageOptionsMenu.tsx @@ -19,6 +19,24 @@ import { MessageSourceCodeItem, } from './message/Message'; +type BaseOptionProps = { + mEvent: MatrixEvent; + room: Room; + mx: MatrixClient; + relations: Relations | undefined; + eventId: string; + 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; + onEditId: ((eventId?: string | undefined) => void) | undefined; + handleAddReactions: MouseEventHandler; + closeMenu: () => void; +}; + export function MessageDropdownMenu({ mEvent, room, From c8319b440268c685d59081cda1a77be5f783c368 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 22:03:26 -0500 Subject: [PATCH 16/73] Add ExtendedProps --- src/app/features/room/MessageOptionsMenu.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/features/room/MessageOptionsMenu.tsx b/src/app/features/room/MessageOptionsMenu.tsx index 9b7fdd2c..267b824b 100644 --- a/src/app/features/room/MessageOptionsMenu.tsx +++ b/src/app/features/room/MessageOptionsMenu.tsx @@ -135,6 +135,17 @@ export function MessageDropdownMenu({ ); } +type ExtendedOptionsProps = BaseOptionProps & { + imagePackRooms: Room[] | undefined; + onActiveStateChange: React.Dispatch>; + menuAnchor: RectCords | undefined; + emojiBoardAnchor: RectCords | undefined; + handleOpenEmojiBoard: MouseEventHandler; + handleOpenMenu: MouseEventHandler; + setMenuAnchor: React.Dispatch>; + setEmojiBoardAnchor: React.Dispatch>; +}; + export function MessageOptionsMenu({ mEvent, room, From 511a16eedc6ca2f704fb5bf7623ad3d0a8ea5695 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 22:03:54 -0500 Subject: [PATCH 17/73] Remove comment from when we embedded the anchors --- src/app/features/room/MessageOptionsMenu.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app/features/room/MessageOptionsMenu.tsx b/src/app/features/room/MessageOptionsMenu.tsx index 267b824b..013903a6 100644 --- a/src/app/features/room/MessageOptionsMenu.tsx +++ b/src/app/features/room/MessageOptionsMenu.tsx @@ -169,10 +169,7 @@ export function MessageOptionsMenu({ handleAddReactions, setMenuAnchor, setEmojiBoardAnchor, -}) { - // const [menuAnchor, setMenuAnchor] = useState(); - // const [emojiBoardAnchor, setEmojiBoardAnchor] = useState(); - +}: ExtendedOptionsProps) { useEffect(() => { onActiveStateChange?.(!!menuAnchor || !!emojiBoardAnchor); }, [emojiBoardAnchor, menuAnchor, onActiveStateChange]); From 460d77d40c4c0012a0b14aeb947ed1320294c8b5 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 22:04:06 -0500 Subject: [PATCH 18/73] Add bottom sheet menu in --- src/app/features/room/MessageOptionsMenu.tsx | 54 ++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/app/features/room/MessageOptionsMenu.tsx b/src/app/features/room/MessageOptionsMenu.tsx index 013903a6..885d7b89 100644 --- a/src/app/features/room/MessageOptionsMenu.tsx +++ b/src/app/features/room/MessageOptionsMenu.tsx @@ -285,3 +285,57 @@ export function MessageOptionsMenu({
); } + +export function BottomSheetMenu({ + isOpen, + onClose, + children, +}: { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; +}) { + return ( +
+ + ); +} + +export function MenuItemButton({ + label, + onClick, + destructive = false, +}: { + label: string; + onClick: () => void; + destructive?: boolean; +}) { + return ( + + + + {label} + + + ); +} From dc296d379f78f21df8ce6b74c7bd3c89b411c252 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 22:04:47 -0500 Subject: [PATCH 19/73] Add prop usage --- src/app/features/room/MessageOptionsMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/features/room/MessageOptionsMenu.tsx b/src/app/features/room/MessageOptionsMenu.tsx index 885d7b89..149fe303 100644 --- a/src/app/features/room/MessageOptionsMenu.tsx +++ b/src/app/features/room/MessageOptionsMenu.tsx @@ -53,7 +53,7 @@ export function MessageDropdownMenu({ onEditId, handleAddReactions, closeMenu, -}) { +}: BaseOptionProps) { return ( {canSendReaction && ( From 05b481187151af1479db682c518610076b8884b3 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 22:04:59 -0500 Subject: [PATCH 20/73] Remove mobile click menu file --- src/app/features/room/MobileClickMenu.tsx | 61 ----------------------- 1 file changed, 61 deletions(-) delete mode 100644 src/app/features/room/MobileClickMenu.tsx diff --git a/src/app/features/room/MobileClickMenu.tsx b/src/app/features/room/MobileClickMenu.tsx deleted file mode 100644 index 48489079..00000000 --- a/src/app/features/room/MobileClickMenu.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable react/destructuring-assignment */ -import React from 'react'; -import { Box, Icon, Icons, Text } from 'folds'; -import classNames from 'classnames'; -import * as css from './RoomTimeline.css'; - -export function BottomSheetMenu({ - isOpen, - onClose, - children, -}: { - isOpen: boolean; - onClose: () => void; - children: React.ReactNode; -}) { - return ( -
- - ); -} - -export function MenuItemButton({ - icon, - label, - onClick, - destructive = false, -}: { - icon: string; - label: string; - onClick: () => void; - destructive?: boolean; -}) { - return ( - - - - {label} - - - ); -} From 2b2332955860c88f68a82117fd17c769ec493c95 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 22:05:11 -0500 Subject: [PATCH 21/73] remove wrongful addition --- .../components/message/layout/layout.css.ts | 68 ------------------- 1 file changed, 68 deletions(-) diff --git a/src/app/components/message/layout/layout.css.ts b/src/app/components/message/layout/layout.css.ts index a61c7f8d..73631129 100644 --- a/src/app/components/message/layout/layout.css.ts +++ b/src/app/components/message/layout/layout.css.ts @@ -197,71 +197,3 @@ export const MessageTextBody = recipe({ }); export type MessageTextBodyVariants = RecipeVariants; - -export const menuBackdrop = style({ - userSelect: 'none', - position: 'fixed', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - transition: 'opacity 0.3s ease-in-out', - opacity: 0, -}); - -export const menuBackdropOpen = style({ - opacity: 1, -}); - -export const menuSheet = style({ - userSelect: 'none', - position: 'fixed', - left: 0, - right: 0, - bottom: 0, - backgroundColor: color.Background.Container, - borderTopLeftRadius: config.radii.R400, - borderTopRightRadius: config.radii.R400, - padding: config.space.S500, - transform: 'translateY(100%)', - transition: 'transform 0.3s ease-out', - boxShadow: '0 -2px 10px rgba(0,0,0,0.1)', -}); - -export const menuSheetOpen = style({ - transform: 'translateY(0)', -}); - -export const menuItem = style({ - userSelect: 'none', - width: '100%', - background: 'none', - border: 'none', - padding: `${config.space.S300} ${config.space.S100}`, - textAlign: 'left', - cursor: 'pointer', - borderRadius: config.radii.R300, - color: color.Primary.ContainerActive, - - outline: 'none', - - WebkitTapHighlightColor: 'transparent', - - WebkitUserSelect: 'none', - MozUserSelect: 'none', - msUserSelect: 'none', - - selectors: { - '&:hover': { - backgroundColor: color.Background.ContainerHover, - }, - '&:focus-visible': { - backgroundColor: color.Background.ContainerHover, - }, - }, -}); - -export const menuItemDestructive = style({ - color: color.Critical.ContainerActive, -}); From d26a901ac9f913eff311011f8aeab108fbab674f Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 22:05:25 -0500 Subject: [PATCH 22/73] add mobilesheet styling here --- src/app/features/room/message/styles.css.ts | 70 ++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/app/features/room/message/styles.css.ts b/src/app/features/room/message/styles.css.ts index b87cb505..d87ea501 100644 --- a/src/app/features/room/message/styles.css.ts +++ b/src/app/features/room/message/styles.css.ts @@ -1,5 +1,5 @@ import { style } from '@vanilla-extract/css'; -import { DefaultReset, config, toRem } from 'folds'; +import { DefaultReset, color, config, toRem } from 'folds'; export const MessageBase = style({ position: 'relative', @@ -48,3 +48,71 @@ export const ReactionsContainer = style({ export const ReactionsTooltipText = style({ wordBreak: 'break-word', }); + +export const menuBackdrop = style({ + userSelect: 'none', + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + transition: 'opacity 0.3s ease-in-out', + opacity: 0, +}); + +export const menuBackdropOpen = style({ + opacity: 1, +}); + +export const menuSheet = style({ + userSelect: 'none', + position: 'fixed', + left: 0, + right: 0, + bottom: 0, + backgroundColor: color.Background.Container, + borderTopLeftRadius: config.radii.R400, + borderTopRightRadius: config.radii.R400, + padding: config.space.S500, + transform: 'translateY(100%)', + transition: 'transform 0.3s ease-out', + boxShadow: '0 -2px 10px rgba(0,0,0,0.1)', +}); + +export const menuSheetOpen = style({ + transform: 'translateY(0)', +}); + +export const menuItem = style({ + userSelect: 'none', + width: '100%', + background: 'none', + border: 'none', + padding: `${config.space.S300} ${config.space.S100}`, + textAlign: 'left', + cursor: 'pointer', + borderRadius: config.radii.R300, + color: color.Primary.ContainerActive, + + outline: 'none', + + WebkitTapHighlightColor: 'transparent', + + WebkitUserSelect: 'none', + MozUserSelect: 'none', + msUserSelect: 'none', + + selectors: { + '&:hover': { + backgroundColor: color.Background.ContainerHover, + }, + '&:focus-visible': { + backgroundColor: color.Background.ContainerHover, + }, + }, +}); + +export const menuItemDestructive = style({ + color: color.Critical.ContainerActive, +}); From c8f490adfb241f87df8d573e8fbc0e04a27edd10 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 22:20:04 -0500 Subject: [PATCH 23/73] Add draggablemessage --- .../room/message/DraggableMessage.tsx | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/app/features/room/message/DraggableMessage.tsx diff --git a/src/app/features/room/message/DraggableMessage.tsx b/src/app/features/room/message/DraggableMessage.tsx new file mode 100644 index 00000000..4476d5c7 --- /dev/null +++ b/src/app/features/room/message/DraggableMessage.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import { useSpring, animated } from '@react-spring/web'; +import { useDrag } from 'react-use-gesture'; +import { Icon, Icons } from 'folds'; + +const DraggableMessageStyles = { + container: { + position: 'relative', + overflow: 'hidden', + width: '100%', + }, + replyIconContainer: { + position: 'absolute', + top: 0, + bottom: 0, + right: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '80px', + }, + messageContent: { + position: 'relative', + touchAction: 'pan-y', + backgroundColor: 'var(--folds-color-Background-Main)', + width: '100%', + }, +}; + +export function DraggableMessage({ children, onReply }) { + const REPLY_THRESHOLD = 80; + + const [{ x, iconScale }, api] = useSpring(() => ({ + x: 0, + iconScale: 0.5, + config: { tension: 250, friction: 25 }, + })); + + const bind = useDrag( + ({ down, movement: [mx], direction: [xDir], vxvy: [vx], cancel }) => { + if (!down && Math.abs(xDir) < 0.7) { + cancel(); + } + + const xTarget = down ? Math.min(0, mx) : 0; + let scaleTarget = down + ? 0.5 + Math.min(Math.abs(mx), REPLY_THRESHOLD) / (REPLY_THRESHOLD * 2) + : 0.5; + + if (mx < -REPLY_THRESHOLD) { + onReply(); + } + + /* + if (!down) { + if (mx < -REPLY_THRESHOLD && vx < -0.5) { + onReply(); + } + } else { + if (mx < -REPLY_THRESHOLD) { + scaleTarget = 1; + } + } +*/ + api.start({ + x: xTarget, + iconScale: scaleTarget, + }); + }, + { + axis: 'x', + filterTaps: true, + threshold: 10, + } + ); + + return ( +
+ `scale(${s})`), + opacity: iconScale.to((s) => (s - 0.5) * 2), + }} + > + + + + + {children} + +
+ ); +} From e8205439518b43113034f0acabb2aa98d22a74fe Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 23:05:06 -0500 Subject: [PATCH 24/73] Add edit handling too (not perfect, but useful later) --- .../room/message/DraggableMessage.tsx | 121 ++++++++++++------ 1 file changed, 79 insertions(+), 42 deletions(-) diff --git a/src/app/features/room/message/DraggableMessage.tsx b/src/app/features/room/message/DraggableMessage.tsx index 4476d5c7..f172afe7 100644 --- a/src/app/features/room/message/DraggableMessage.tsx +++ b/src/app/features/room/message/DraggableMessage.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useSpring, animated } from '@react-spring/web'; import { useDrag } from 'react-use-gesture'; import { Icon, Icons } from 'folds'; +import { MatrixClient, MatrixEvent } from 'matrix-js-sdk'; const DraggableMessageStyles = { container: { @@ -9,7 +10,7 @@ const DraggableMessageStyles = { overflow: 'hidden', width: '100%', }, - replyIconContainer: { + iconContainer: { position: 'absolute', top: 0, bottom: 0, @@ -17,7 +18,7 @@ const DraggableMessageStyles = { display: 'flex', alignItems: 'center', justifyContent: 'center', - width: '80px', + width: '150px', }, messageContent: { position: 'relative', @@ -25,46 +26,76 @@ const DraggableMessageStyles = { backgroundColor: 'var(--folds-color-Background-Main)', width: '100%', }, + icon: { + position: 'absolute', + }, }; -export function DraggableMessage({ children, onReply }) { +export function DraggableMessage({ + children, + onReply, + onEdit, + event, + mx, +}: { + children: React.ReactNode; + onReply: () => void; + onEdit: () => void; + event: MatrixEvent; + mx: MatrixClient; +}) { + const canEdit = mx.getUserId() === event.getSender(); const REPLY_THRESHOLD = 80; + const EDIT_THRESHOLD = canEdit ? 250 : Infinity; - const [{ x, iconScale }, api] = useSpring(() => ({ + const [{ x, replyOpacity, editOpacity, iconScale }, api] = useSpring(() => ({ x: 0, + replyOpacity: 0, + editOpacity: 0, iconScale: 0.5, config: { tension: 250, friction: 25 }, })); const bind = useDrag( - ({ down, movement: [mx], direction: [xDir], vxvy: [vx], cancel }) => { - if (!down && Math.abs(xDir) < 0.7) { - cancel(); - } - - const xTarget = down ? Math.min(0, mx) : 0; - let scaleTarget = down - ? 0.5 + Math.min(Math.abs(mx), REPLY_THRESHOLD) / (REPLY_THRESHOLD * 2) - : 0.5; - - if (mx < -REPLY_THRESHOLD) { - onReply(); - } - - /* + ({ down, movement: [x], vxvy: [vx] }) => { if (!down) { - if (mx < -REPLY_THRESHOLD && vx < -0.5) { + const finalDistance = Math.abs(x); + + if (finalDistance > EDIT_THRESHOLD) { + onEdit(); + } else if (finalDistance > REPLY_THRESHOLD) { onReply(); } - } else { - if (mx < -REPLY_THRESHOLD) { - scaleTarget = 1; - } } -*/ + + const xTarget = down ? Math.min(0, x) : 0; + const distance = Math.abs(xTarget); + + let newReplyOpacity = 0; + let newEditOpacity = 0; + let newScale = 1.0; + + if (canEdit && distance > REPLY_THRESHOLD) { + newReplyOpacity = 0; + newEditOpacity = 1; + if (down && distance > EDIT_THRESHOLD) { + newScale = 1.1; + } + } else { + newReplyOpacity = 1; + newEditOpacity = 0; + newScale = 0.5 + (distance / REPLY_THRESHOLD) * 0.5; + } + + if (distance < 5) { + newReplyOpacity = 0; + } + api.start({ x: xTarget, - iconScale: scaleTarget, + replyOpacity: newReplyOpacity, + editOpacity: newEditOpacity, + iconScale: newScale, }); }, { @@ -76,23 +107,29 @@ export function DraggableMessage({ children, onReply }) { return (
- `scale(${s})`), - opacity: iconScale.to((s) => (s - 0.5) * 2), - }} - > - - +
+ `scale(${s})`), + }} + > + + - + `scale(${s})`), + }} + > + + +
+ + {children}
From 41b036550fc2379a9302cb721fe7ee4262f0c03f Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 23:05:16 -0500 Subject: [PATCH 25/73] Remove event id refs --- src/app/features/room/MessageOptionsMenu.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/app/features/room/MessageOptionsMenu.tsx b/src/app/features/room/MessageOptionsMenu.tsx index 149fe303..98c4f680 100644 --- a/src/app/features/room/MessageOptionsMenu.tsx +++ b/src/app/features/room/MessageOptionsMenu.tsx @@ -24,7 +24,6 @@ type BaseOptionProps = { room: Room; mx: MatrixClient; relations: Relations | undefined; - eventId: string; canSendReaction: boolean | undefined; canEdit: boolean | undefined; canDelete: boolean | undefined; @@ -42,7 +41,6 @@ export function MessageDropdownMenu({ room, mx, relations, - eventId, canSendReaction, canEdit, canDelete, @@ -59,7 +57,7 @@ export function MessageDropdownMenu({ {canSendReaction && ( { - onReactionToggle(eventId, key, shortcode); + onReactionToggle(mEvent.getId() ?? '', key, shortcode); closeMenu(); }} /> @@ -84,7 +82,7 @@ export function MessageDropdownMenu({ size="300" after={} radii="300" - data-event-id={eventId} + data-event-id={mEvent.getId()} onClick={(evt) => { onReplyClick(evt); closeMenu(); @@ -99,9 +97,9 @@ export function MessageDropdownMenu({ size="300" after={} radii="300" - data-event-id={eventId} + data-event-id={mEvent.getId()} onClick={() => { - onEditId(eventId); + onEditId(mEvent.getId()); closeMenu(); }} > @@ -111,7 +109,7 @@ export function MessageDropdownMenu({ )} {!hideReadReceipts && ( - + )} @@ -255,7 +253,6 @@ export function MessageOptionsMenu({ room={room} mx={mx} relations={relations} - eventId={eventId} canSendReaction={canSendReaction} canEdit={canEdit} canDelete={canDelete} From 7b84264b2639c2601c2a77543ee71910c22b7b45 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 23:06:20 -0500 Subject: [PATCH 26/73] Add draggable handling --- src/app/features/room/message/Message.tsx | 114 +++++++++++++++++----- 1 file changed, 92 insertions(+), 22 deletions(-) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 148b75aa..a2f169f1 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -80,9 +80,9 @@ import { StateEvent } from '../../../../types/matrix/room'; import { getTagIconSrc, PowerLevelTag } from '../../../hooks/usePowerLevelTags'; import { PowerIcon } from '../../../components/power'; import colorMXID from '../../../../util/colorMXID'; -import { MessageDropdownMenu, MessageOptionsMenu } from '../MessageOptionsMenu'; -import { BottomSheetMenu } from '../MobileClickMenu'; +import { MessageDropdownMenu, MessageOptionsMenu, BottomSheetMenu } from '../MessageOptionsMenu'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; +import { DraggableMessage } from './DraggableMessage'; export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void; @@ -719,13 +719,13 @@ export const Message = as<'div', MessageProps>( const useAuthentication = useMediaAuthentication(); const senderId = mEvent.getSender() ?? ''; const { hoverProps, isHovered } = useHover({}); - const { focusWithinProps, isFocusWithin } = useFocusWithin({}); + const { focusWithinProps } = useFocusWithin({}); const [menuAnchor, setMenuAnchor] = useState(); const [emojiBoardAnchor, setEmojiBoardAnchor] = useState(); const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; const [isDesktopOptionsActive, setDesktopOptionsActive] = useState(false); - const showDesktopOptions = !isMobile && (isHovered || isFocusWithin || isDesktopOptionsActive); + //const showDesktopOptions = !isMobile && (isHovered || isFocusWithin || isDesktopOptionsActive); const [isMobileSheetOpen, setMobileSheetOpen] = useState(false); const senderDisplayName = getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; @@ -764,6 +764,10 @@ export const Message = as<'div', MessageProps>( handleClose(); }; + // TODO: Remove this and clean it up later... + const button = document.createElement('button'); + button.setAttribute('data-event-id', mEvent.getId()); + const tagColor = powerLevelTag?.color ? accessibleTagColors?.get(powerLevelTag.color) : undefined; @@ -926,23 +930,90 @@ export const Message = as<'div', MessageProps>( handleOpenMenu={handleOpenMenu} /> )} - {messageLayout === MessageLayout.Compact && ( - - {msgContentJSX} - - )} - {messageLayout === MessageLayout.Bubble && ( - - {headerJSX} - {msgContentJSX} - - )} - {messageLayout !== MessageLayout.Compact && messageLayout !== MessageLayout.Bubble && ( - - {headerJSX} - {msgContentJSX} - - )} + {messageLayout === MessageLayout.Compact && + (!isMobile ? ( + + {msgContentJSX} + + ) : ( + { + const mockTargetElement = document.createElement('button'); + mockTargetElement.setAttribute('data-event-id', mEvent.getId()); + const mockEvent = { + currentTarget: mockTargetElement, + }; + + onReplyClick(mockEvent); + }} + onEdit={() => { + onEditId(mEvent.getId()); + }} + mx={mx} + > + + {msgContentJSX} + + + ))} + {messageLayout === MessageLayout.Bubble && + (!isMobile ? ( + + {msgContentJSX} + + ) : ( + { + const mockTargetElement = document.createElement('button'); + mockTargetElement.setAttribute('data-event-id', mEvent.getId()); + const mockEvent = { + currentTarget: mockTargetElement, + }; + + onReplyClick(mockEvent); + }} + onEdit={() => { + onEditId(mEvent.getId()); + }} + mx={mx} + > + + {msgContentJSX} + + + ))} + {messageLayout !== MessageLayout.Compact && + messageLayout !== MessageLayout.Bubble && + (!isMobile ? ( + + {headerJSX} + {msgContentJSX} + + ) : ( + { + const mockTargetElement = document.createElement('button'); + mockTargetElement.setAttribute('data-event-id', mEvent.getId()); + const mockEvent = { + currentTarget: mockTargetElement, + }; + + onReplyClick(mockEvent); + }} + onEdit={() => { + onEditId(mEvent.getId()); + }} + mx={mx} + > + + {headerJSX} + {msgContentJSX} + + + ))} {isMobile && ( @@ -954,7 +1025,6 @@ export const Message = as<'div', MessageProps>( setMobileSheetOpen(false); }} mEvent={mEvent} - eventId={mEvent.getId()} room={room} mx={mx} relations={relations} From c4d0a7a8209b91b0df4901e9f85e8e8cbdf70a40 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 23:20:09 -0500 Subject: [PATCH 27/73] Fix ref issue causing the menu to remain open unless an option is selected --- src/app/features/room/MessageOptionsMenu.tsx | 45 +++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/app/features/room/MessageOptionsMenu.tsx b/src/app/features/room/MessageOptionsMenu.tsx index 98c4f680..f5372485 100644 --- a/src/app/features/room/MessageOptionsMenu.tsx +++ b/src/app/features/room/MessageOptionsMenu.tsx @@ -1,5 +1,5 @@ import { Box, Icon, IconButton, Icons, Line, Menu, MenuItem, PopOut, RectCords, Text } from 'folds'; -import React, { MouseEventHandler, useEffect } from 'react'; +import React, { forwardRef, MouseEventHandler, useEffect } from 'react'; import FocusTrap from 'focus-trap-react'; import classNames from 'classnames'; @@ -36,24 +36,27 @@ type BaseOptionProps = { closeMenu: () => void; }; -export function MessageDropdownMenu({ - mEvent, - room, - mx, - relations, - canSendReaction, - canEdit, - canDelete, - canPinEvent, - hideReadReceipts, - onReactionToggle, - onReplyClick, - onEditId, - handleAddReactions, - closeMenu, -}: BaseOptionProps) { - return ( - +export const MessageDropdownMenu = forwardRef( + ( + { + mEvent, + room, + mx, + relations, + canSendReaction, + canEdit, + canDelete, + canPinEvent, + hideReadReceipts, + onReactionToggle, + onReplyClick, + onEditId, + handleAddReactions, + closeMenu, + }, + ref + ) => ( + {canSendReaction && ( { @@ -130,8 +133,8 @@ export function MessageDropdownMenu({ )} - ); -} + ) +); type ExtendedOptionsProps = BaseOptionProps & { imagePackRooms: Room[] | undefined; From 8396771e3fc84b561288ebb3037c6760b3d1fb75 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 23:21:28 -0500 Subject: [PATCH 28/73] remove comment --- src/app/features/room/MessageOptionsMenu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/features/room/MessageOptionsMenu.tsx b/src/app/features/room/MessageOptionsMenu.tsx index f5372485..c4e145c6 100644 --- a/src/app/features/room/MessageOptionsMenu.tsx +++ b/src/app/features/room/MessageOptionsMenu.tsx @@ -118,7 +118,6 @@ export const MessageDropdownMenu = forwardRef( {canPinEvent && } - {/* Redact and Report actions */} {((!mEvent.isRedacted() && canDelete) || mEvent.getSender() !== mx.getUserId()) && ( <> From 1fe7f014d6577f02791214ccc6f030f436a882a0 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 23:27:42 -0500 Subject: [PATCH 29/73] bump package.json to include some new react pieces --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 7622dbc8..0083b8de 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0", "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3", "@fontsource/inter": "4.5.14", + "@react-spring/web": "10.0.1", "@tanstack/react-query": "5.24.1", "@tanstack/react-query-devtools": "5.24.1", "@tanstack/react-virtual": "3.2.0", @@ -74,6 +75,7 @@ "react-modal": "3.16.1", "react-range": "1.8.14", "react-router-dom": "6.20.0", + "react-use-gesture": "9.1.3", "sanitize-html": "2.12.1", "slate": "0.112.0", "slate-dom": "0.112.2", From c9fd33b2dc9a5619fc634f87cd9d97733c0d122e Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 15 Jun 2025 23:27:52 -0500 Subject: [PATCH 30/73] update package lock --- package-lock.json | 84 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3d2e8d05..d05ca387 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0", "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3", "@fontsource/inter": "4.5.14", + "@react-spring/web": "10.0.1", "@tanstack/react-query": "5.24.1", "@tanstack/react-query-devtools": "5.24.1", "@tanstack/react-virtual": "3.2.0", @@ -63,6 +64,7 @@ "react-modal": "3.16.1", "react-range": "1.8.14", "react-router-dom": "6.20.0", + "react-use-gesture": "9.1.3", "sanitize-html": "2.12.1", "slate": "0.112.0", "slate-dom": "0.112.2", @@ -3098,6 +3100,78 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@react-spring/animated": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.1.tgz", + "integrity": "sha512-BGL3hA66Y8Qm3KmRZUlfG/mFbDPYajgil2/jOP0VXf2+o2WPVmcDps/eEgdDqgf5Pv9eBbyj7LschLMuSjlW3Q==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.0.1.tgz", + "integrity": "sha512-KaMMsN1qHuVTsFpg/5ajAVye7OEqhYbCq0g4aKM9bnSZlDBBYpO7Uf+9eixyXN8YEbF+YXaYj9eoWDs+npZ+sA==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.1.tgz", + "integrity": "sha512-UrzG/d6Is+9i0aCAjsjWRqIlFFiC4lFqFHrH63zK935z2YDU95TOFio4VKGISJ5SG0xq4ULy7c1V3KU+XvL+Yg==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.1.tgz", + "integrity": "sha512-KR2tmjDShPruI/GGPfAZOOLvDgkhFseabjvxzZFFggJMPkyICLjO0J6mCIoGtdJSuHywZyc4Mmlgi+C88lS00g==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.0.1.tgz", + "integrity": "sha512-Fk1wYVAKL+ZTYK+4YFDpHf3Slsy59pfFFvnnTfRjQQFGlyIo4VejPtDs3CbDiuBjM135YztRyZjIH2VbycB+ZQ==", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-10.0.1.tgz", + "integrity": "sha512-FgQk02OqFrYyJBTTnBTWAU0WPzkHkKXauc6aeexcvATvLapUxwnfGuLlsLYF8BYjEVfkivPT04ziAue6zyRBtQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/core": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-stately/calendar": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.7.0.tgz", @@ -9848,6 +9922,16 @@ "react-dom": ">=16.8" } }, + "node_modules/react-use-gesture": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-9.1.3.tgz", + "integrity": "sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==", + "deprecated": "This package is no longer maintained. Please use @use-gesture/react instead", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", From fb73ebbb3b86fbb8737b6ed098eff41c9a835771 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 03:33:15 -0500 Subject: [PATCH 31/73] Started on adding the context menu for rooms in the nav on mobile --- src/app/features/room-nav/RoomNavItem.tsx | 214 ++++++++++++++++------ 1 file changed, 159 insertions(+), 55 deletions(-) diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index bdb81418..a9bec223 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -19,6 +19,8 @@ import { } from 'folds'; import { useFocusWithin, useHover } from 'react-aria'; import FocusTrap from 'focus-trap-react'; +import { useParams } from 'react-router-dom'; +import { useLongPress } from 'use-long-press'; import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav'; import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; @@ -49,6 +51,10 @@ import { RoomNotificationMode, } from '../../hooks/useRoomsNotificationPreferences'; import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher'; +import { useCallState } from '../../pages/client/call/CallProvider'; +import { useRoomNavigate } from '../../hooks/useRoomNavigate'; +import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; +import { BottomSheetMenu } from '../room/MessageOptionsMenu'; type RoomNavItemMenuProps = { room: Room; @@ -65,6 +71,8 @@ const RoomNavItemMenu = forwardRef( const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const openRoomSettings = useOpenRoomSettings(); const space = useSpaceOptionally(); + const screenSize = useScreenSizeContext(); + const isMobile = screenSize === ScreenSize.Mobile; const handleMarkAsRead = () => { markAsRead(mx, room.roomId, hideActivity); @@ -89,7 +97,7 @@ const RoomNavItemMenu = forwardRef( }; return ( - + receipt.userId !== mx.getUserId() ); + const { navigateRoom } = useRoomNavigate(); + const { roomIdOrAlias: viewedRoomId } = useParams(); + const screenSize = useScreenSizeContext(); + const isMobile = screenSize === ScreenSize.Mobile; + const [isMobileSheetOpen, setMobileSheetOpen] = useState(false); + + const longPressBinder = useLongPress( + () => { + if (isMobile) { + setMobileSheetOpen(true); + } + }, + { + threshold: 400, + cancelOnMovement: true, + } + ); const handleContextMenu: MouseEventHandler = (evt) => { evt.preventDefault(); + + if (isMobile) { + // return; + } setMenuAnchor({ x: evt.clientX, y: evt.clientY, @@ -235,21 +264,58 @@ export function RoomNavItem({ setMenuAnchor(evt.currentTarget.getBoundingClientRect()); }; - const optionsVisible = hover || !!menuAnchor; + const handleNavItemClick: MouseEventHandler = (evt) => { + const target = evt.target as HTMLElement; + const chatButton = (evt.currentTarget as HTMLElement).querySelector( + '[data-testid="chat-button"]' + ); + if (chatButton && chatButton.contains(target)) { + return; + } + if (room.isCallRoom()) { + if (!isMobile) { + if (activeCallRoomId !== room.roomId) { + if (mx.getRoom(viewedRoomId)?.isCallRoom()) { + navigateRoom(room.roomId); + } + hangUp(room.roomId); + setActiveCallRoomId(room.roomId); + } else { + navigateRoom(room.roomId); + } + } else { + evt.stopPropagation(); + if (isChatOpen) toggleChat(); + setViewedCallRoomId(room.roomId); + navigateRoom(room.roomId); + } + } else { + navigateRoom(room.roomId); + } + }; + + const handleChatButtonClick = (evt: MouseEvent) => { + evt.stopPropagation(); + if (!isChatOpen) toggleChat(); + setViewedCallRoomId(room.roomId); + }; + + const optionsVisible = !isMobile && (hover || !!menuAnchor); return ( - - - + <> + + {showAvatar ? ( @@ -273,6 +339,7 @@ export function RoomNavItem({ filled={selected} size="100" joinRule={room.getJoinRule()} + call={room.isCallRoom()} /> )} @@ -296,48 +363,85 @@ export function RoomNavItem({ )} - - {optionsVisible && ( - - setMenuAnchor(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', - isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', - escapeDeactivates: stopPropagation, - }} - > - setMenuAnchor(undefined)} - notificationMode={notificationMode} - /> - - } - > - + setMenuAnchor(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt) => evt.key === 'ArrowDown', + isKeyBackward: (evt) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, + }} + > + setMenuAnchor(undefined)} + notificationMode={notificationMode} + /> + + } > - - - - + {room.isCallRoom() && ( + + Open chat + + } + > + {(triggerRef) => ( + + + + + + )} + + )} + + + + + + )} + + {isMobile && ( + setMobileSheetOpen(false)}> + setMobileSheetOpen(false)} + notificationMode={notificationMode} + /> + )} - + ); } From 3abbffbb899bca87f4f58a8e8d6c578c08f6ffd5 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 03:33:32 -0500 Subject: [PATCH 32/73] Make nav button text non-interactive --- src/app/features/room-nav/styles.css.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/features/room-nav/styles.css.ts b/src/app/features/room-nav/styles.css.ts index b5701a6f..c1097380 100644 --- a/src/app/features/room-nav/styles.css.ts +++ b/src/app/features/room-nav/styles.css.ts @@ -3,6 +3,7 @@ import { config } from 'folds'; export const CategoryButton = style({ flexGrow: 1, + userSelect: 'none', }); export const CategoryButtonIcon = style({ opacity: config.opacity.P400, From 8ae0b75abd8afc226bb9c55af788a4d877a6111e Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 10:25:50 -0500 Subject: [PATCH 33/73] Add Tooltip Provider --- src/app/features/room-nav/RoomNavItem.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index a9bec223..ac902136 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -16,6 +16,8 @@ import { RectCords, Badge, Spinner, + Tooltip, + TooltipProvider, } from 'folds'; import { useFocusWithin, useHover } from 'react-aria'; import FocusTrap from 'focus-trap-react'; @@ -250,7 +252,7 @@ export function RoomNavItem({ evt.preventDefault(); if (isMobile) { - // return; + // return; } setMenuAnchor({ x: evt.clientX, From 9b97d02c217f9226c33918c7217342cc7957e337 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 13:04:21 -0500 Subject: [PATCH 34/73] 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 --- .../room/message/MessageOptionsMenu.tsx | 356 ++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 src/app/features/room/message/MessageOptionsMenu.tsx diff --git a/src/app/features/room/message/MessageOptionsMenu.tsx b/src/app/features/room/message/MessageOptionsMenu.tsx new file mode 100644 index 00000000..d0c7277b --- /dev/null +++ b/src/app/features/room/message/MessageOptionsMenu.tsx @@ -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; + onEditId: ((eventId?: string | undefined) => void) | undefined; + handleAddReactions: MouseEventHandler; + closeMenu: () => void; +}; + +export const MessageDropdownMenu = forwardRef( + ( + { + mEvent, + room, + mx, + relations, + canSendReaction, + canEdit, + canDelete, + canPinEvent, + hideReadReceipts, + onReactionToggle, + onReplyClick, + onEditId, + handleAddReactions, + closeMenu, + }, + ref + ) => ( + + {canSendReaction && ( + { + onReactionToggle(mEvent.getId() ?? '', key, shortcode); + closeMenu(); + }} + /> + )} + + {canSendReaction && ( + } + radii="300" + onClick={handleAddReactions} + > + + Add Reaction + + + )} + {relations && ( + + )} + } + radii="300" + data-event-id={mEvent.getId()} + onClick={(evt) => { + onReplyClick(evt); + closeMenu(); + }} + > + + Reply + + + {canEdit && onEditId && ( + } + radii="300" + data-event-id={mEvent.getId()} + onClick={() => { + onEditId(mEvent.getId()); + closeMenu(); + }} + > + + Edit Message + + + )} + {!hideReadReceipts && ( + + )} + + + {canPinEvent && } + + {((!mEvent.isRedacted() && canDelete) || mEvent.getSender() !== mx.getUserId()) && ( + <> + + + {!mEvent.isRedacted() && canDelete && ( + + )} + {mEvent.getSender() !== mx.getUserId() && ( + + )} + + + )} + + ) +); + +type ExtendedOptionsProps = BaseOptionProps & { + imagePackRooms: Room[] | undefined; + onActiveStateChange: React.Dispatch>; + menuAnchor: RectCords | undefined; + emojiBoardAnchor: RectCords | undefined; + handleOpenEmojiBoard: MouseEventHandler; + handleOpenMenu: MouseEventHandler; + setMenuAnchor: React.Dispatch>; + setEmojiBoardAnchor: React.Dispatch>; + 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 ( + setMobileSheetOpen(false)} isOpen={isMobileSheetOpen}> + {view === 'options' ? ( + { + closeMenu(); + setMobileSheetOpen(false); + }} + handleAddReactions={() => setView('emoji')} + /> + ) : ( + +
+ setView('options')}> + + + + Add Reaction + +
+ { + 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)} + /> +
+ )} +
+ ); + } + + return ( +
+ + + {canSendReaction && ( + { + onReactionToggle(eventId, key); + setEmojiBoardAnchor(undefined); + }} + onCustomEmojiSelect={(mxc, shortcode) => { + onReactionToggle(eventId, mxc, shortcode); + setEmojiBoardAnchor(undefined); + }} + requestClose={() => setEmojiBoardAnchor(undefined)} + /> + } + > + + + + + )} + + + + {canEdit && onEditId && ( + onEditId(eventId)} + variant="SurfaceVariant" + size="300" + radii="300" + > + + + )} + setMenuAnchor(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt) => evt.key === 'ArrowDown', + isKeyBackward: (evt) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, + }} + > + + + } + > + + + + + + +
+ ); +} From 1cf4dae929e4b323a0393374d2112470b0b81304 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 13:05:37 -0500 Subject: [PATCH 35/73] move file --- .../room/message/DraggableMessage.tsx | 137 ------------------ 1 file changed, 137 deletions(-) delete mode 100644 src/app/features/room/message/DraggableMessage.tsx diff --git a/src/app/features/room/message/DraggableMessage.tsx b/src/app/features/room/message/DraggableMessage.tsx deleted file mode 100644 index f172afe7..00000000 --- a/src/app/features/room/message/DraggableMessage.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React from 'react'; -import { useSpring, animated } from '@react-spring/web'; -import { useDrag } from 'react-use-gesture'; -import { Icon, Icons } from 'folds'; -import { MatrixClient, MatrixEvent } from 'matrix-js-sdk'; - -const DraggableMessageStyles = { - container: { - position: 'relative', - overflow: 'hidden', - width: '100%', - }, - iconContainer: { - position: 'absolute', - top: 0, - bottom: 0, - right: 0, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: '150px', - }, - messageContent: { - position: 'relative', - touchAction: 'pan-y', - backgroundColor: 'var(--folds-color-Background-Main)', - width: '100%', - }, - icon: { - position: 'absolute', - }, -}; - -export function DraggableMessage({ - children, - onReply, - onEdit, - event, - mx, -}: { - children: React.ReactNode; - onReply: () => void; - onEdit: () => void; - event: MatrixEvent; - mx: MatrixClient; -}) { - const canEdit = mx.getUserId() === event.getSender(); - const REPLY_THRESHOLD = 80; - const EDIT_THRESHOLD = canEdit ? 250 : Infinity; - - const [{ x, replyOpacity, editOpacity, iconScale }, api] = useSpring(() => ({ - x: 0, - replyOpacity: 0, - editOpacity: 0, - iconScale: 0.5, - config: { tension: 250, friction: 25 }, - })); - - const bind = useDrag( - ({ down, movement: [x], vxvy: [vx] }) => { - if (!down) { - const finalDistance = Math.abs(x); - - if (finalDistance > EDIT_THRESHOLD) { - onEdit(); - } else if (finalDistance > REPLY_THRESHOLD) { - onReply(); - } - } - - const xTarget = down ? Math.min(0, x) : 0; - const distance = Math.abs(xTarget); - - let newReplyOpacity = 0; - let newEditOpacity = 0; - let newScale = 1.0; - - if (canEdit && distance > REPLY_THRESHOLD) { - newReplyOpacity = 0; - newEditOpacity = 1; - if (down && distance > EDIT_THRESHOLD) { - newScale = 1.1; - } - } else { - newReplyOpacity = 1; - newEditOpacity = 0; - newScale = 0.5 + (distance / REPLY_THRESHOLD) * 0.5; - } - - if (distance < 5) { - newReplyOpacity = 0; - } - - api.start({ - x: xTarget, - replyOpacity: newReplyOpacity, - editOpacity: newEditOpacity, - iconScale: newScale, - }); - }, - { - axis: 'x', - filterTaps: true, - threshold: 10, - } - ); - - return ( -
-
- `scale(${s})`), - }} - > - - - - `scale(${s})`), - }} - > - - -
- - - {children} - -
- ); -} From 1ec2c2d27b7ab9d57f7fea7bcb49cd74acd2fc90 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 13:06:06 -0500 Subject: [PATCH 36/73] Change behavior based on if is mobile or not and move to be a component and not a feature --- .../message/behavior/DraggableMessage.tsx | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/app/components/message/behavior/DraggableMessage.tsx diff --git a/src/app/components/message/behavior/DraggableMessage.tsx b/src/app/components/message/behavior/DraggableMessage.tsx new file mode 100644 index 00000000..0d303277 --- /dev/null +++ b/src/app/components/message/behavior/DraggableMessage.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { useSpring, animated } from '@react-spring/web'; +import { useDrag } from 'react-use-gesture'; +import { Icon, Icons } from 'folds'; +import { MatrixClient, MatrixEvent } from 'matrix-js-sdk'; +import { container, iconContainer, messageContent, icon } from './style.css'; +import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; + +export function DraggableMessage({ + children, + onReply, + onEdit, + event, + mx, +}: { + children: React.ReactNode; + onReply: () => void; + onEdit: () => void; + event: MatrixEvent; + mx: MatrixClient; +}) { + const screenSize = useScreenSizeContext(); + const isMobile = screenSize === ScreenSize.Mobile; + + const canEdit = mx.getUserId() === event.getSender(); + const REPLY_THRESHOLD = 80; + const EDIT_THRESHOLD = canEdit ? 250 : Infinity; + + const [{ x, replyOpacity, editOpacity, iconScale }, api] = useSpring(() => ({ + x: 0, + replyOpacity: 0, + editOpacity: 0, + iconScale: 0.5, + config: { tension: 250, friction: 25 }, + })); + + const bind = useDrag( + ({ down, movement: [mvx], vxvy: [vx] }) => { + if (!down) { + const finalDistance = Math.abs(mvx); + + if (finalDistance > EDIT_THRESHOLD) { + onEdit(); + } else if (finalDistance > REPLY_THRESHOLD) { + onReply(); + } + } + + const xTarget = down ? Math.min(0, mvx) : 0; + const distance = Math.abs(xTarget); + + let newReplyOpacity = 0; + let newEditOpacity = 0; + let newScale = 1.0; + + if (canEdit && distance > REPLY_THRESHOLD) { + newReplyOpacity = 0; + newEditOpacity = 1; + if (down && distance > EDIT_THRESHOLD) { + newScale = 1.1; + } + } else { + newReplyOpacity = 1; + newEditOpacity = 0; + newScale = 0.5 + (distance / REPLY_THRESHOLD) * 0.5; + } + + if (distance < 5) { + newReplyOpacity = 0; + } + + api.start({ + x: xTarget, + replyOpacity: newReplyOpacity, + editOpacity: newEditOpacity, + iconScale: newScale, + }); + }, + { + axis: 'x', + filterTaps: true, + threshold: 10, + } + ); + if (isMobile) { + return ( +
+
+ `scale(${s})`), + }} + > + + + + `scale(${s})`), + }} + > + + +
+ + + {children} + +
+ ); + } + return
{children}
; +} From 3e814fc294438cf029bfbd50b33ce34be3f18f34 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 13:06:32 -0500 Subject: [PATCH 37/73] Add style for draggable --- .../components/message/behavior/style.css.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/app/components/message/behavior/style.css.ts diff --git a/src/app/components/message/behavior/style.css.ts b/src/app/components/message/behavior/style.css.ts new file mode 100644 index 00000000..39df357c --- /dev/null +++ b/src/app/components/message/behavior/style.css.ts @@ -0,0 +1,29 @@ +import { style } from '@vanilla-extract/css'; + +export const container = style({ + position: 'relative', + overflow: 'hidden', + width: '100%', +}); + +export const iconContainer = style({ + position: 'absolute', + top: 0, + bottom: 0, + right: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '150px', +}); + +export const messageContent = style({ + position: 'relative', + touchAction: 'pan-y', + backgroundColor: 'var(--folds-color-Background-Main)', + width: '100%', +}); + +export const icon = style({ + position: 'absolute', +}); From c94074423d2ec349a678b47f88f4d4429a7dc6b6 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 13:08:14 -0500 Subject: [PATCH 38/73] remove unused imports --- src/app/features/room/message/Message.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index a2f169f1..4042c993 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -66,7 +66,6 @@ import * as css from './styles.css'; import { EventReaders } from '../../../components/event-readers'; import { TextViewer } from '../../../components/text-viewer'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; -import { EmojiBoard } from '../../../components/emoji-board'; import { ReactionViewer } from '../reaction-viewer'; import { MessageEditor } from './MessageEditor'; import { UserAvatar } from '../../../components/user-avatar'; @@ -80,7 +79,7 @@ import { StateEvent } from '../../../../types/matrix/room'; import { getTagIconSrc, PowerLevelTag } from '../../../hooks/usePowerLevelTags'; import { PowerIcon } from '../../../components/power'; import colorMXID from '../../../../util/colorMXID'; -import { MessageDropdownMenu, MessageOptionsMenu, BottomSheetMenu } from '../MessageOptionsMenu'; +import { MessageOptionsMenu } from './MessageOptionsMenu'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { DraggableMessage } from './DraggableMessage'; From 1a7e59641b539d29ad2e6e0594a49eb0c226646d Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 13:08:24 -0500 Subject: [PATCH 39/73] file moved --- src/app/features/room/message/Message.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 4042c993..3f6b46b1 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -81,7 +81,7 @@ import { PowerIcon } from '../../../components/power'; import colorMXID from '../../../../util/colorMXID'; import { MessageOptionsMenu } from './MessageOptionsMenu'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; -import { DraggableMessage } from './DraggableMessage'; +import { DraggableMessage } from '../../../components/message/behavior/DraggableMessage'; export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void; From 4a1446ca74bda0e2de2541d9cb7dc3b04b2219d4 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 13:09:20 -0500 Subject: [PATCH 40/73] Remove unused --- src/app/features/room/message/Message.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 3f6b46b1..cff0f03b 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -724,12 +724,10 @@ export const Message = as<'div', MessageProps>( 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 = getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; const senderAvatarMxc = getMemberAvatarMxc(room, senderId); - const [view, setView] = useState('options'); const closeMenu = () => { setMenuAnchor(undefined); From 5dd67009cba815d1d7a9a3d127339f044d73c1c3 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 13:09:45 -0500 Subject: [PATCH 41/73] remove unused methods --- src/app/features/room/message/Message.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index cff0f03b..7a462861 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -751,16 +751,6 @@ export const Message = as<'div', MessageProps>( }, 100); }; - const handleClose = () => { - setView('options'); - closeMenu(); - }; - - const onEmojiSelect = (key, shortcode) => { - onReactionToggle(mEvent.getId(), key, shortcode); - handleClose(); - }; - // TODO: Remove this and clean it up later... const button = document.createElement('button'); button.setAttribute('data-event-id', mEvent.getId()); From 2cdc4a49694a168b1be59e9ce5e66bc624db4f9d Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 13:10:40 -0500 Subject: [PATCH 42/73] Reduce reuse and clean up as a result of handling the behavior in OptionsMenu --- src/app/features/room/message/Message.tsx | 281 +++++++++------------- 1 file changed, 113 insertions(+), 168 deletions(-) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 7a462861..bef5ea13 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -877,187 +877,132 @@ export const Message = as<'div', MessageProps>( }; return ( - <> - - {!edit && (isHovered || !!menuAnchor || !!emojiBoardAnchor) && ( - + {!edit && (isMobileSheetOpen || isHovered || !!menuAnchor || !!emojiBoardAnchor) && ( + + )} + {messageLayout === MessageLayout.Compact && + (!isMobile ? ( + + {msgContentJSX} + + ) : ( + { + const mockTargetElement = document.createElement('button'); + mockTargetElement.setAttribute('data-event-id', mEvent.getId()); + const mockEvent = { + currentTarget: mockTargetElement, + }; + + onReplyClick(mockEvent); + }} + onEdit={() => { + onEditId(mEvent.getId()); + }} mx={mx} - relations={relations} - imagePackRooms={imagePackRooms} - canSendReaction={canSendReaction} - canEdit={canEditEvent(mx, mEvent)} - canDelete={canDelete} - canPinEvent={canPinEvent} - hideReadReceipts={hideReadReceipts} - onReactionToggle={onReactionToggle} - onReplyClick={onReplyClick} - onEditId={onEditId} - onActiveStateChange={setDesktopOptionsActive} - handleAddReactions={handleAddReactions} - closeMenu={closeMenu} - emojiBoardAnchor={emojiBoardAnchor} - menuAnchor={menuAnchor} - handleOpenEmojiBoard={handleOpenEmojiBoard} - setEmojiBoardAnchor={setEmojiBoardAnchor} - setMenuAnchor={setMenuAnchor} - handleOpenMenu={handleOpenMenu} - /> - )} - {messageLayout === MessageLayout.Compact && - (!isMobile ? ( + > {msgContentJSX} - ) : ( - { - const mockTargetElement = document.createElement('button'); - mockTargetElement.setAttribute('data-event-id', mEvent.getId()); - const mockEvent = { - currentTarget: mockTargetElement, - }; + + ))} + {messageLayout === MessageLayout.Bubble && + (!isMobile ? ( + + {msgContentJSX} + + ) : ( + { + const mockTargetElement = document.createElement('button'); + mockTargetElement.setAttribute('data-event-id', mEvent.getId()); + const mockEvent = { + currentTarget: mockTargetElement, + }; - onReplyClick(mockEvent); - }} - onEdit={() => { - onEditId(mEvent.getId()); - }} - mx={mx} - > - - {msgContentJSX} - - - ))} - {messageLayout === MessageLayout.Bubble && - (!isMobile ? ( + onReplyClick(mockEvent); + }} + onEdit={() => { + onEditId(mEvent.getId()); + }} + mx={mx} + > {msgContentJSX} - ) : ( - { - const mockTargetElement = document.createElement('button'); - mockTargetElement.setAttribute('data-event-id', mEvent.getId()); - const mockEvent = { - currentTarget: mockTargetElement, - }; + + ))} + {messageLayout !== MessageLayout.Compact && + messageLayout !== MessageLayout.Bubble && + (!isMobile ? ( + + {headerJSX} + {msgContentJSX} + + ) : ( + { + const mockTargetElement = document.createElement('button'); + mockTargetElement.setAttribute('data-event-id', mEvent.getId()); + const mockEvent = { + currentTarget: mockTargetElement, + }; - onReplyClick(mockEvent); - }} - onEdit={() => { - onEditId(mEvent.getId()); - }} - mx={mx} - > - - {msgContentJSX} - - - ))} - {messageLayout !== MessageLayout.Compact && - messageLayout !== MessageLayout.Bubble && - (!isMobile ? ( + onReplyClick(mockEvent); + }} + onEdit={() => { + onEditId(mEvent.getId()); + }} + mx={mx} + > {headerJSX} {msgContentJSX} - ) : ( - { - const mockTargetElement = document.createElement('button'); - mockTargetElement.setAttribute('data-event-id', mEvent.getId()); - const mockEvent = { - currentTarget: mockTargetElement, - }; - - onReplyClick(mockEvent); - }} - onEdit={() => { - onEditId(mEvent.getId()); - }} - mx={mx} - > - - {headerJSX} - {msgContentJSX} - - - ))} - - - {isMobile && ( - setMobileSheetOpen(false)} isOpen={isMobileSheetOpen}> - {view === 'options' ? ( - { - closeMenu(); - setMobileSheetOpen(false); - }} - mEvent={mEvent} - 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={() => setView('emoji')} - /> - ) : ( - -
- setView('options')}> - - - - Add Reaction - -
- { - 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)} - /> -
- )} -
- )} - + + ))} + ); } ); From 4085732d39b50fe06a0dc2c39f67b3ae5a7abf5d Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 13:11:04 -0500 Subject: [PATCH 43/73] still trying to find a place for this to live --- .../room/message/MobileContextMenu.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/app/features/room/message/MobileContextMenu.tsx diff --git a/src/app/features/room/message/MobileContextMenu.tsx b/src/app/features/room/message/MobileContextMenu.tsx new file mode 100644 index 00000000..3711b72c --- /dev/null +++ b/src/app/features/room/message/MobileContextMenu.tsx @@ -0,0 +1,32 @@ +import classNames from 'classnames'; +import React from 'react'; +import * as css from './styles.css'; + +export function BottomSheetMenu({ + isOpen, + onClose, + children, +}: { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; +}) { + return ( +
+ + ); +} From d13c2458468d0706d69c2ebc4e6fae01dabd85d0 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 17:47:13 -0500 Subject: [PATCH 44/73] Category buttons shouldn't be highlightable --- src/app/features/room-nav/styles.css.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/features/room-nav/styles.css.ts b/src/app/features/room-nav/styles.css.ts index c1097380..8b96eebf 100644 --- a/src/app/features/room-nav/styles.css.ts +++ b/src/app/features/room-nav/styles.css.ts @@ -4,6 +4,7 @@ import { config } from 'folds'; export const CategoryButton = style({ flexGrow: 1, userSelect: 'none', + WebkitUserSelect: 'none', }); export const CategoryButtonIcon = style({ opacity: config.opacity.P400, From ccb41c47a3f6e2446586195b59f3a673d5f5a510 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 20:13:07 -0500 Subject: [PATCH 45/73] Update CSS to disable text select on mobile (might be better suited elsewhere) --- src/app/features/room/message/styles.css.ts | 76 +++------------------ 1 file changed, 8 insertions(+), 68 deletions(-) diff --git a/src/app/features/room/message/styles.css.ts b/src/app/features/room/message/styles.css.ts index d87ea501..e0545bd6 100644 --- a/src/app/features/room/message/styles.css.ts +++ b/src/app/features/room/message/styles.css.ts @@ -3,6 +3,14 @@ import { DefaultReset, color, config, toRem } from 'folds'; export const MessageBase = style({ position: 'relative', + '@media': { + 'screen and (max-width: 768px)': { + userSelect: 'none', + WebkitUserSelect: 'none', + MozUserSelect: 'none', + msUserSelect: 'none', + }, + }, }); export const MessageOptionsBase = style([ @@ -48,71 +56,3 @@ export const ReactionsContainer = style({ export const ReactionsTooltipText = style({ wordBreak: 'break-word', }); - -export const menuBackdrop = style({ - userSelect: 'none', - position: 'fixed', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - transition: 'opacity 0.3s ease-in-out', - opacity: 0, -}); - -export const menuBackdropOpen = style({ - opacity: 1, -}); - -export const menuSheet = style({ - userSelect: 'none', - position: 'fixed', - left: 0, - right: 0, - bottom: 0, - backgroundColor: color.Background.Container, - borderTopLeftRadius: config.radii.R400, - borderTopRightRadius: config.radii.R400, - padding: config.space.S500, - transform: 'translateY(100%)', - transition: 'transform 0.3s ease-out', - boxShadow: '0 -2px 10px rgba(0,0,0,0.1)', -}); - -export const menuSheetOpen = style({ - transform: 'translateY(0)', -}); - -export const menuItem = style({ - userSelect: 'none', - width: '100%', - background: 'none', - border: 'none', - padding: `${config.space.S300} ${config.space.S100}`, - textAlign: 'left', - cursor: 'pointer', - borderRadius: config.radii.R300, - color: color.Primary.ContainerActive, - - outline: 'none', - - WebkitTapHighlightColor: 'transparent', - - WebkitUserSelect: 'none', - MozUserSelect: 'none', - msUserSelect: 'none', - - selectors: { - '&:hover': { - backgroundColor: color.Background.ContainerHover, - }, - '&:focus-visible': { - backgroundColor: color.Background.ContainerHover, - }, - }, -}); - -export const menuItemDestructive = style({ - color: color.Critical.ContainerActive, -}); From c269e841f6739f1524a5f4b2d3a5f9c1240fc7b5 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 20:15:26 -0500 Subject: [PATCH 46/73] restructure to use different rendering strat --- .../room/message/MessageOptionsMenu.tsx | 112 ++++++++++-------- 1 file changed, 64 insertions(+), 48 deletions(-) diff --git a/src/app/features/room/message/MessageOptionsMenu.tsx b/src/app/features/room/message/MessageOptionsMenu.tsx index d0c7277b..bc229ede 100644 --- a/src/app/features/room/message/MessageOptionsMenu.tsx +++ b/src/app/features/room/message/MessageOptionsMenu.tsx @@ -31,7 +31,11 @@ import { MessageSourceCodeItem, } from './Message'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; -import { BottomSheetMenu } from './MobileContextMenu'; +import MobileContextMenu from '../../../molecules/mobile-context-menu/MobileContextMenu'; + +const EmojiBoard = lazy(() => + import('../../../components/emoji-board').then((module) => ({ default: module.EmojiBoard })) +); type BaseOptionProps = { mEvent: MatrixEvent; @@ -85,7 +89,10 @@ export const MessageDropdownMenu = forwardRef( size="300" after={} radii="300" - onClick={handleAddReactions} + onClick={(e) => { + handleAddReactions(e); + closeMenu(); + }} > Add Reaction @@ -93,7 +100,7 @@ export const MessageDropdownMenu = forwardRef( )} {relations && ( - + {}} /> )} ( type ExtendedOptionsProps = BaseOptionProps & { imagePackRooms: Room[] | undefined; - onActiveStateChange: React.Dispatch>; menuAnchor: RectCords | undefined; emojiBoardAnchor: RectCords | undefined; handleOpenEmojiBoard: MouseEventHandler; handleOpenMenu: MouseEventHandler; setMenuAnchor: React.Dispatch>; setEmojiBoardAnchor: React.Dispatch>; - isMobileSheetOpen; - setMobileSheetOpen; + isOptionsMenuOpen; + setOptionsMenuOpen; + isEmojiBoardOpen; + setEmojiBoardOpen; }; export function MessageOptionsMenu({ @@ -176,7 +184,6 @@ export function MessageOptionsMenu({ onReactionToggle, onReplyClick, onEditId, - onActiveStateChange, closeMenu, menuAnchor, emojiBoardAnchor, @@ -185,13 +192,11 @@ export function MessageOptionsMenu({ handleAddReactions, setMenuAnchor, setEmojiBoardAnchor, - isMobileSheetOpen, - setMobileSheetOpen, + isOptionsMenuOpen, + setOptionsMenuOpen, + isEmojiBoardOpen, + setEmojiBoardOpen, }: ExtendedOptionsProps) { - useEffect(() => { - onActiveStateChange?.(!!menuAnchor || !!emojiBoardAnchor); - }, [emojiBoardAnchor, menuAnchor, onActiveStateChange]); - const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; const [view, setView] = useState('options'); @@ -218,47 +223,58 @@ export function MessageOptionsMenu({ if (isMobile) { return ( - setMobileSheetOpen(false)} isOpen={isMobileSheetOpen}> - {view === 'options' ? ( - { + <> + {isOptionsMenuOpen && ( + { closeMenu(); - setMobileSheetOpen(false); }} - handleAddReactions={() => setView('emoji')} - /> - ) : ( - -
- setView('options')}> - - - - Add Reaction - -
- { - onReactionToggle(mEvent.getId(), key); - setEmojiBoardAnchor(undefined); + isOpen={isOptionsMenuOpen} + > + { closeMenu(); - setMobileSheetOpen(false); }} - onCustomEmojiSelect={(mxc, shortcode) => { - onReactionToggle(mEvent.getId(), mxc, shortcode); - setEmojiBoardAnchor(undefined); - closeMenu(); - setMobileSheetOpen(false); - }} - requestClose={() => setEmojiBoardAnchor(undefined)} + handleAddReactions={handleAddReactions} /> -
+
)} -
+ + {isEmojiBoardOpen && ( + { + closeMenu(); + setEmojiBoardOpen(false); + }} + isOpen={isEmojiBoardOpen} + > + Loading

}> + { + onReactionToggle(mEvent.getId(), key); + setEmojiBoardAnchor(undefined); + setEmojiBoardOpen(false); + (document.activeElement as HTMLElement)?.blur(); + }} + onCustomEmojiSelect={(mxc, shortcode) => { + onReactionToggle(mEvent.getId(), mxc, shortcode); + setEmojiBoardAnchor(undefined); + setEmojiBoardOpen(false); + (document.activeElement as HTMLElement)?.blur(); + }} + requestClose={() => { + setEmojiBoardAnchor(undefined); + setEmojiBoardOpen(false); + }} + /> +
+
+ )} + ); } From 49259c37b01a026a8a1ecd65b703a8f25e7e284c Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 20:15:55 -0500 Subject: [PATCH 47/73] simplify by placing check in draggable base --- src/app/features/room/message/Message.tsx | 204 +++++++++++----------- 1 file changed, 98 insertions(+), 106 deletions(-) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index bef5ea13..eea14245 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -721,15 +721,18 @@ export const Message = as<'div', MessageProps>( const { focusWithinProps } = useFocusWithin({}); const [menuAnchor, setMenuAnchor] = useState(); const [emojiBoardAnchor, setEmojiBoardAnchor] = useState(); + const [isDesktopOptionsActive, setDesktopOptionsActive] = useState(false); + const [isOptionsMenuOpen, setOptionsMenuOpen] = useState(false); + const [isEmojiBoardOpen, setEmojiBoardOpen] = useState(false); + const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; - const [isDesktopOptionsActive, setDesktopOptionsActive] = useState(false); - const [isMobileSheetOpen, setMobileSheetOpen] = useState(false); const senderDisplayName = getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; const senderAvatarMxc = getMemberAvatarMxc(room, senderId); const closeMenu = () => { + setOptionsMenuOpen(false); setMenuAnchor(undefined); }; @@ -745,10 +748,10 @@ export const Message = as<'div', MessageProps>( const handleAddReactions: MouseEventHandler = () => { const rect = menuAnchor; - closeMenu(); + setEmojiBoardOpen(true); setTimeout(() => { setEmojiBoardAnchor(rect); - }, 100); + }, 150); }; // TODO: Remove this and clean it up later... @@ -767,7 +770,7 @@ export const Message = as<'div', MessageProps>( const longPressBinder = useLongPress( () => { if (isMobile) { - setMobileSheetOpen(true); + setOptionsMenuOpen(true); } }, { @@ -883,125 +886,114 @@ export const Message = as<'div', MessageProps>( space={messageSpacing} collapse={collapse} highlight={highlight} - selected={isDesktopOptionsActive || isMobileSheetOpen} + selected={isDesktopOptionsActive || isOptionsMenuOpen || isEmojiBoardOpen} {...props} {...hoverProps} - {...focusWithinProps} + {...(!isMobile ? focusWithinProps : {})} {...(isMobile ? longPressBinder() : {})} ref={ref} > - {!edit && (isMobileSheetOpen || isHovered || !!menuAnchor || !!emojiBoardAnchor) && ( - + )} + {messageLayout === MessageLayout.Compact && ( + { + const mockTargetElement = document.createElement('button'); + mockTargetElement.setAttribute('data-event-id', mEvent.getId()); + const mockEvent = { + currentTarget: mockTargetElement, + }; + + onReplyClick(mockEvent); + }} + onEdit={() => { + onEditId(mEvent.getId()); + }} mx={mx} - relations={relations} - imagePackRooms={imagePackRooms} - canSendReaction={canSendReaction} - canEdit={canEditEvent(mx, mEvent)} - canDelete={canDelete} - canPinEvent={canPinEvent} - hideReadReceipts={hideReadReceipts} - onReactionToggle={onReactionToggle} - onReplyClick={onReplyClick} - onEditId={onEditId} - onActiveStateChange={setDesktopOptionsActive} - handleAddReactions={handleAddReactions} - closeMenu={closeMenu} - emojiBoardAnchor={emojiBoardAnchor} - menuAnchor={menuAnchor} - handleOpenEmojiBoard={handleOpenEmojiBoard} - setEmojiBoardAnchor={setEmojiBoardAnchor} - setMenuAnchor={setMenuAnchor} - handleOpenMenu={handleOpenMenu} - setMobileSheetOpen={setMobileSheetOpen} - isMobileSheetOpen={isMobileSheetOpen} - /> - )} - {messageLayout === MessageLayout.Compact && - (!isMobile ? ( + > {msgContentJSX} - ) : ( - { - const mockTargetElement = document.createElement('button'); - mockTargetElement.setAttribute('data-event-id', mEvent.getId()); - const mockEvent = { - currentTarget: mockTargetElement, - }; + + )} + {messageLayout === MessageLayout.Bubble && ( + { + const mockTargetElement = document.createElement('button'); + mockTargetElement.setAttribute('data-event-id', mEvent.getId()); + const mockEvent = { + currentTarget: mockTargetElement, + }; - onReplyClick(mockEvent); - }} - onEdit={() => { - onEditId(mEvent.getId()); - }} - mx={mx} - > - - {msgContentJSX} - - - ))} - {messageLayout === MessageLayout.Bubble && - (!isMobile ? ( + onReplyClick(mockEvent); + }} + onEdit={() => { + onEditId(mEvent.getId()); + }} + mx={mx} + > {msgContentJSX} - ) : ( - { - const mockTargetElement = document.createElement('button'); - mockTargetElement.setAttribute('data-event-id', mEvent.getId()); - const mockEvent = { - currentTarget: mockTargetElement, - }; + + )} + {messageLayout !== MessageLayout.Compact && messageLayout !== MessageLayout.Bubble && ( + { + const mockTargetElement = document.createElement('button'); + mockTargetElement.setAttribute('data-event-id', mEvent.getId()); + const mockEvent = { + currentTarget: mockTargetElement, + }; - onReplyClick(mockEvent); - }} - onEdit={() => { - onEditId(mEvent.getId()); - }} - mx={mx} - > - - {msgContentJSX} - - - ))} - {messageLayout !== MessageLayout.Compact && - messageLayout !== MessageLayout.Bubble && - (!isMobile ? ( + onReplyClick(mockEvent); + }} + onEdit={() => { + onEditId(mEvent.getId()); + }} + mx={mx} + > {headerJSX} {msgContentJSX} - ) : ( - { - const mockTargetElement = document.createElement('button'); - mockTargetElement.setAttribute('data-event-id', mEvent.getId()); - const mockEvent = { - currentTarget: mockTargetElement, - }; - - onReplyClick(mockEvent); - }} - onEdit={() => { - onEditId(mEvent.getId()); - }} - mx={mx} - > - - {headerJSX} - {msgContentJSX} - - - ))} + + )} ); } From 9074f4135a562d32709c26ad9dccc9f9dd51ad46 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 20:16:14 -0500 Subject: [PATCH 48/73] update imports --- src/app/features/room/message/MessageOptionsMenu.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/features/room/message/MessageOptionsMenu.tsx b/src/app/features/room/message/MessageOptionsMenu.tsx index bc229ede..2fc508bf 100644 --- a/src/app/features/room/message/MessageOptionsMenu.tsx +++ b/src/app/features/room/message/MessageOptionsMenu.tsx @@ -11,12 +11,10 @@ import { RectCords, Text, } from 'folds'; -import React, { forwardRef, MouseEventHandler, useEffect, useState } from 'react'; +import React, { forwardRef, lazy, MouseEventHandler, Suspense, 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'; From b8ec0d4ba0eef9c5ddd5cfe43fa5e01530edaf2f Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 20:16:32 -0500 Subject: [PATCH 49/73] remove no longer needed css --- src/app/components/message/layout/layout.css.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/app/components/message/layout/layout.css.ts b/src/app/components/message/layout/layout.css.ts index 73631129..290e8466 100644 --- a/src/app/components/message/layout/layout.css.ts +++ b/src/app/components/message/layout/layout.css.ts @@ -183,17 +183,6 @@ export const MessageTextBody = recipe({ }, }, }, - - '@media': { - 'screen and (max-width: 768px)': { - base: { - userSelect: 'none', - WebkitUserSelect: 'none', - MozUserSelect: 'none', - msUserSelect: 'none', - }, - }, - }, }); export type MessageTextBodyVariants = RecipeVariants; From a1805a9ca90989d2cdc753a3d761b0db87ac53c4 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 20:16:55 -0500 Subject: [PATCH 50/73] Use new menu component on roomnav --- src/app/features/room-nav/RoomNavItem.tsx | 303 +++++++++++----------- 1 file changed, 153 insertions(+), 150 deletions(-) diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index ac902136..886301ee 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -56,7 +56,7 @@ import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationS import { useCallState } from '../../pages/client/call/CallProvider'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; -import { BottomSheetMenu } from '../room/MessageOptionsMenu'; +import { MobileContextMenu } from '../../molecules/mobile-context-menu/MobileContextMenu'; type RoomNavItemMenuProps = { room: Room; @@ -99,7 +99,7 @@ const RoomNavItemMenu = forwardRef( }; return ( - + { - if (isMobile) { - setMobileSheetOpen(true); - } - }, - { - threshold: 400, - cancelOnMovement: true, - } - ); - const handleContextMenu: MouseEventHandler = (evt) => { evt.preventDefault(); @@ -302,148 +290,163 @@ export function RoomNavItem({ setViewedCallRoomId(room.roomId); }; + const handleCloseMenu = () => { + setMenuAnchor(undefined); + setMobileSheetOpen(false); + }; + const optionsVisible = !isMobile && (hover || !!menuAnchor); + const longPressBinder = useLongPress( + () => { + if (isMobile) { + setMobileSheetOpen(true); + } + }, + { + threshold: 400, + cancelOnMovement: true, + } + ); + + const menuContent = ( + + ); + return ( - <> - - - - - {showAvatar ? ( - ( - - {nameInitials(room.name)} - - )} - /> - ) : ( - - )} - - - - {room.name} - - - {!optionsVisible && !unread && !selected && typingMember.length > 0 && ( - - - - )} - {!optionsVisible && unread && ( - - 0} count={unread.total} /> - - )} - {!optionsVisible && notificationMode !== RoomNotificationMode.Unset && ( - + + + + + {showAvatar ? ( + ( + + {nameInitials(room.name)} + + )} + /> + ) : ( + )} + + + + {room.name} + - - {optionsVisible && ( - - setMenuAnchor(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt) => evt.key === 'ArrowDown', - isKeyBackward: (evt) => evt.key === 'ArrowUp', - escapeDeactivates: stopPropagation, - }} - > - setMenuAnchor(undefined)} - notificationMode={notificationMode} - /> - - } - > - {room.isCallRoom() && ( - - Open chat - - } - > - {(triggerRef) => ( - - - - - - )} - - )} - 0 && ( + + + + )} + {!optionsVisible && unread && ( + + 0} count={unread.total} /> + + )} + {!optionsVisible && notificationMode !== RoomNotificationMode.Unset && ( + + )} + + + {optionsVisible && ( + + setMenuAnchor(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', + isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, + }} > - - - - - )} - - {isMobile && ( - setMobileSheetOpen(false)}> - setMobileSheetOpen(false)} - notificationMode={notificationMode} - /> - + {menuContent} + + } + > + {room.isCallRoom() && ( + + Open chat + + } + > + {(triggerRef) => ( + + + + + + )} + + )} + + + + + )} - + {isMobile && ( + + {menuContent} + + )} + ); } From e2e3ae63e36c5f7a58aafea7e235d2b8f0920575 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 20:17:20 -0500 Subject: [PATCH 51/73] add mobilecontextmenu --- .../mobile-context-menu/MobileContextMenu.jsx | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/app/molecules/mobile-context-menu/MobileContextMenu.jsx diff --git a/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx b/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx new file mode 100644 index 00000000..e2915e7c --- /dev/null +++ b/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx @@ -0,0 +1,62 @@ +import React, { useEffect } from 'react'; +import { useSpring, animated } from '@react-spring/web'; +import { useDrag } from 'react-use-gesture'; +import './MobileContextMenu.scss'; + +export function MobileContextMenu({ isOpen, onClose, children }) { + const { innerHeight } = window; + + const [{ y }, api] = useSpring(() => ({ + y: innerHeight, + config: { tension: 250, friction: 25 }, + })); + + useEffect(() => { + api.start({ y: isOpen ? 0 : innerHeight }); + }, [api, innerHeight, isOpen]); + + const bind = useDrag( + ({ last, movement: [, my], velocities: [, vy] }) => { + if (last) { + if (my > innerHeight / 4) { + onClose(); + } else { + api.start({ y: 0 }); + } + } else { + api.start({ y: Math.max(my, 0), immediate: true }); + } + }, + { + from: () => [0, y.get()], + filterTaps: true, + bounds: { top: 0 }, + rubberband: true, + } + ); + if (!isOpen) return null; + + return ( + <> + + + +
+
{children}
+ + + ); +} + +export default MobileContextMenu; From a572e6c352decd3e946e025dbcdb0c8520881eb6 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 20:17:29 -0500 Subject: [PATCH 52/73] css for mobile context menu --- .../MobileContextMenu.scss | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/app/molecules/mobile-context-menu/MobileContextMenu.scss diff --git a/src/app/molecules/mobile-context-menu/MobileContextMenu.scss b/src/app/molecules/mobile-context-menu/MobileContextMenu.scss new file mode 100644 index 00000000..f42e1f75 --- /dev/null +++ b/src/app/molecules/mobile-context-menu/MobileContextMenu.scss @@ -0,0 +1,34 @@ +.bottom-sheet-backdrop { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + touch-action: none; +} + +.bottom-sheet-container { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 1001; + display: flex; + flex-direction: column; + max-height: 90vh; + border-radius: 16px 16px 0 0; + box-shadow: 0px -4px B16px rgba(0, 0, 0, 0.15); +} + +.bottom-sheet-grabber { + flex-shrink: 0; + width: 40px; + height: 5px; + border-radius: 2.5px; + background-color: #ccc; + margin: 8px auto; +} + +.bottom-sheet-content { + overflow-y: auto; + padding: 0 1rem 1rem 1rem; +} From 01c3b23284a83b2c563139c29b534923b9d45d98 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 20:26:07 -0500 Subject: [PATCH 53/73] revert --- src/app/features/room-nav/styles.css.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/features/room-nav/styles.css.ts b/src/app/features/room-nav/styles.css.ts index 8b96eebf..b5701a6f 100644 --- a/src/app/features/room-nav/styles.css.ts +++ b/src/app/features/room-nav/styles.css.ts @@ -3,8 +3,6 @@ import { config } from 'folds'; export const CategoryButton = style({ flexGrow: 1, - userSelect: 'none', - WebkitUserSelect: 'none', }); export const CategoryButtonIcon = style({ opacity: config.opacity.P400, From f9ff976624713fd99d3b7840bbe78ba218177893 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 20:26:24 -0500 Subject: [PATCH 54/73] disable highlight on nav buttons (for webkit) --- src/app/components/virtualizer/style.css.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/components/virtualizer/style.css.ts b/src/app/components/virtualizer/style.css.ts index 962550cc..1be1fac6 100644 --- a/src/app/components/virtualizer/style.css.ts +++ b/src/app/components/virtualizer/style.css.ts @@ -4,6 +4,7 @@ import { DefaultReset } from 'folds'; export const VirtualTile = style([ DefaultReset, { + WebkitUserSelect: 'none', position: 'absolute', width: '100%', left: 0, From 699bae67203e0974720c86f7e11c906d14edc2bc Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Tue, 17 Jun 2025 20:27:11 -0500 Subject: [PATCH 55/73] You basically should never be copying from this anyway --- src/app/components/virtualizer/style.css.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/components/virtualizer/style.css.ts b/src/app/components/virtualizer/style.css.ts index 1be1fac6..c93636a8 100644 --- a/src/app/components/virtualizer/style.css.ts +++ b/src/app/components/virtualizer/style.css.ts @@ -5,6 +5,8 @@ export const VirtualTile = style([ DefaultReset, { WebkitUserSelect: 'none', + MozUserSelect: 'none', + userSelect: 'none', position: 'absolute', width: '100%', left: 0, From 46958d88c98ec0f8b4ee31b116eabd153d88000a Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 01:11:11 -0500 Subject: [PATCH 56/73] add imports --- src/app/pages/client/sidebar/SpaceTabs.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 011741ee..68445c69 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -39,6 +39,8 @@ import { import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'; import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; import FocusTrap from 'focus-trap-react'; +import { useLongPress } from 'use-long-press'; +import { createPortal } from 'react-dom'; import { useOrphanSpaces, useRecursiveChildScopeFactory, @@ -91,6 +93,7 @@ import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings'; +import { MobileContextMenu } from '../../../molecules/mobile-context-menu/MobileContextMenu'; type SpaceMenuProps = { room: Room; From 37638ff81826f091be6d375bd4e3931da2f51361 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 01:11:25 -0500 Subject: [PATCH 57/73] remove width cap to allow scaling in mobile --- src/app/pages/client/sidebar/SpaceTabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 68445c69..305bfa64 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -145,7 +145,7 @@ const SpaceMenu = forwardRef( }; return ( - + Date: Wed, 18 Jun 2025 01:12:17 -0500 Subject: [PATCH 58/73] update useDraggableItem to allow function passing to impact drag behavior --- src/app/pages/client/sidebar/SpaceTabs.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 305bfa64..32c65bcd 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -224,7 +224,8 @@ const useDraggableItem = ( item: SidebarDraggable, targetRef: RefObject, onDragging: (item?: SidebarDraggable) => void, - dragHandleRef?: RefObject + dragHandleRef?: RefObject, + onActualDragStart?: () => void ): boolean => { const [dragging, setDragging] = useState(false); @@ -241,13 +242,16 @@ const useDraggableItem = ( onDragStart: () => { setDragging(true); onDragging?.(item); + if (typeof onActualDragStart === 'function') { + onActualDragStart(); + } }, onDrop: () => { setDragging(false); onDragging?.(undefined); }, }); - }, [targetRef, dragHandleRef, item, onDragging]); + }, [targetRef, dragHandleRef, item, onDragging, onActualDragStart]); return dragging; }; From 81a4113ec35f12454cdcf094d2c6b723b26287a7 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 01:12:30 -0500 Subject: [PATCH 59/73] add our mobile checks --- src/app/pages/client/sidebar/SpaceTabs.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 32c65bcd..71a87f9f 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -395,6 +395,11 @@ function SpaceTab({ const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const targetRef = useRef(null); + const screenSize = useScreenSizeContext(); + const isMobile = screenSize === ScreenSize.Mobile; + const [isMobileSheetOpen, setMobileSheetOpen] = useState(false); + + const [menuAnchor, setMenuAnchor] = useState(); const spaceDraggable: SidebarDraggable = useMemo( () => From 868e4a8a90a81a6ab268a5e1dbddc5fd746bdc36 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 01:13:25 -0500 Subject: [PATCH 60/73] swap to set useDraggableItem as a variable so we can refer to it in our long press --- src/app/pages/client/sidebar/SpaceTabs.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 71a87f9f..32a6debd 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -412,7 +412,21 @@ function SpaceTab({ [folder, space] ); - useDraggableItem(spaceDraggable, targetRef, onDragging); + const handleDragStart = useCallback(() => { + if (isMobileSheetOpen) { + setMenuAnchor(undefined); + setMobileSheetOpen(false); + } + }, [isMobileSheetOpen]); + + const isDragging = useDraggableItem( + spaceDraggable, + targetRef, + onDragging, + undefined, + handleDragStart + ); + const dropState = useDropTarget(spaceDraggable, targetRef); const dropType = dropState?.type; From 6c6fd89911e1e8bfe193ceea6891fe3b088a75d7 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 01:13:33 -0500 Subject: [PATCH 61/73] move menu anchor --- src/app/pages/client/sidebar/SpaceTabs.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 32a6debd..01b00c45 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -430,8 +430,6 @@ function SpaceTab({ const dropState = useDropTarget(spaceDraggable, targetRef); const dropType = dropState?.type; - const [menuAnchor, setMenuAnchor] = useState(); - const handleContextMenu: MouseEventHandler = (evt) => { evt.preventDefault(); const cords = evt.currentTarget.getBoundingClientRect(); From 550534ae31a3802f487b56687d65f29da9241a67 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 01:13:39 -0500 Subject: [PATCH 62/73] add long press --- src/app/pages/client/sidebar/SpaceTabs.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 01b00c45..2f3055ce 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -439,6 +439,18 @@ function SpaceTab({ }); }; + const longPressBinder = useLongPress( + () => { + if (isMobile && !isDragging) { + setMobileSheetOpen(true); + } + }, + { + threshold: 400, + cancelOnMovement: true, + } + ); + return ( {(unread) => ( From 7cd2a4a143cd4532cf505b072971f0568686e3a4 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 01:13:50 -0500 Subject: [PATCH 63/73] add prop for listening to long presses --- src/app/pages/client/sidebar/SpaceTabs.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 2f3055ce..5ebbfbab 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -462,6 +462,7 @@ function SpaceTab({ data-drop-above={dropType === 'reorder-above'} data-drop-below={dropType === 'reorder-below'} data-inside-folder={!!folder} + {...(isMobile ? longPressBinder() : {})} > {(triggerRef) => ( From 69c916f1b89207f11065ec44939ef8b25ad4d1b6 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 01:14:13 -0500 Subject: [PATCH 64/73] add mobile context menu --- src/app/pages/client/sidebar/SpaceTabs.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 5ebbfbab..a66a9d41 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -516,6 +516,21 @@ function SpaceTab({ } /> )} + {createPortal( + { + setMobileSheetOpen(false); + }} + isOpen={isMobileSheetOpen} + > + setMobileSheetOpen(false)} + onUnpin={onUnpin} + /> + , + document.body + )} )} From 94eacffd99baf74a9d4e0c6130521a7a30422b86 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 01:31:29 -0500 Subject: [PATCH 65/73] no highlighting on elements that should never be highlighted --- src/app/components/sidebar/Sidebar.css.ts | 25 ++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/app/components/sidebar/Sidebar.css.ts b/src/app/components/sidebar/Sidebar.css.ts index c3686223..59856a98 100644 --- a/src/app/components/sidebar/Sidebar.css.ts +++ b/src/app/components/sidebar/Sidebar.css.ts @@ -9,7 +9,9 @@ export const Sidebar = style([ width: toRem(66), backgroundColor: color.Background.Container, borderRight: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`, - + MozUserSelect: 'none', + WebkitUserSelect: 'none', + userSelect: 'none', display: 'flex', flexDirection: 'column', color: color.Background.OnContainer, @@ -19,6 +21,9 @@ export const Sidebar = style([ export const SidebarStack = style([ DefaultReset, { + MozUserSelect: 'none', + WebkitUserSelect: 'none', + userSelect: 'none', width: '100%', display: 'flex', flexDirection: 'column', @@ -68,6 +73,9 @@ export const SidebarItem = recipe({ base: [ DefaultReset, { + MozUserSelect: 'none', + WebkitUserSelect: 'none', + userSelect: 'none', minWidth: toRem(42), display: 'flex', alignItems: 'center', @@ -101,6 +109,9 @@ export const SidebarItem = recipe({ ], variants: { active: { + MozUserSelect: 'none', + WebkitUserSelect: 'none', + userSelect: 'none', true: { selectors: { '&::before': { @@ -148,6 +159,9 @@ export type SidebarItemBadgeVariants = RecipeVariants; export const SidebarAvatar = recipe({ base: [ { + MozUserSelect: 'none', + WebkitUserSelect: 'none', + userSelect: 'none', selectors: { 'button&': { cursor: 'pointer', @@ -158,6 +172,9 @@ export const SidebarAvatar = recipe({ variants: { size: { '200': { + MozUserSelect: 'none', + WebkitUserSelect: 'none', + userSelect: 'none', width: toRem(16), height: toRem(16), fontSize: toRem(10), @@ -165,10 +182,16 @@ export const SidebarAvatar = recipe({ letterSpacing: config.letterSpacing.T200, }, '300': { + MozUserSelect: 'none', + WebkitUserSelect: 'none', + userSelect: 'none', width: toRem(34), height: toRem(34), }, '400': { + MozUserSelect: 'none', + WebkitUserSelect: 'none', + userSelect: 'none', width: toRem(42), height: toRem(42), }, From 1e4fccb826e1279f70f90c6e8ac55866655cb3dc Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 19:57:05 -0500 Subject: [PATCH 66/73] Fixes the drag jitter on Android and only the grabber working after the virtual keyboard appears once --- .../mobile-context-menu/MobileContextMenu.jsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx b/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx index e2915e7c..41ef7a3c 100644 --- a/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx +++ b/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx @@ -5,6 +5,14 @@ import './MobileContextMenu.scss'; export function MobileContextMenu({ isOpen, onClose, children }) { const { innerHeight } = window; + useEffect(() => { + if (isOpen) { + document.body.style.overscrollBehavior = 'contain'; + } + return () => { + document.body.style.overscrollBehavior = 'auto'; + }; + }, [isOpen]); const [{ y }, api] = useSpring(() => ({ y: innerHeight, @@ -49,11 +57,13 @@ export function MobileContextMenu({ isOpen, onClose, children }) { {...bind()} style={{ y, - touchAction: 'pan-y', + touchAction: 'none', }} >
-
{children}
+
+ {children} +
); From 791381c978331e760b4ad7e2275343312b2aed95 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 20:03:11 -0500 Subject: [PATCH 67/73] swallow events under the menu if we actually drag --- src/app/molecules/mobile-context-menu/MobileContextMenu.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx b/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx index 41ef7a3c..fd809fa0 100644 --- a/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx +++ b/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx @@ -24,9 +24,11 @@ export function MobileContextMenu({ isOpen, onClose, children }) { }, [api, innerHeight, isOpen]); const bind = useDrag( - ({ last, movement: [, my], velocities: [, vy] }) => { + ({ last, movement: [, my], event }) => { if (last) { if (my > innerHeight / 4) { + event.preventDefault(); + event.stopPropagation(); onClose(); } else { api.start({ y: 0 }); From 9e7b37dcd1f14f30b53f85ba103db30567480905 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 20:53:10 -0500 Subject: [PATCH 68/73] Clear the UI up to accurate reflect the thresholds --- .../message/behavior/DraggableMessage.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/components/message/behavior/DraggableMessage.tsx b/src/app/components/message/behavior/DraggableMessage.tsx index 0d303277..31394d4e 100644 --- a/src/app/components/message/behavior/DraggableMessage.tsx +++ b/src/app/components/message/behavior/DraggableMessage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useSpring, animated } from '@react-spring/web'; import { useDrag } from 'react-use-gesture'; import { Icon, Icons } from 'folds'; @@ -23,19 +23,20 @@ export function DraggableMessage({ const isMobile = screenSize === ScreenSize.Mobile; const canEdit = mx.getUserId() === event.getSender(); - const REPLY_THRESHOLD = 80; - const EDIT_THRESHOLD = canEdit ? 250 : Infinity; + const REPLY_THRESHOLD = 30; + const EDIT_THRESHOLD = canEdit ? 180 : Infinity; - const [{ x, replyOpacity, editOpacity, iconScale }, api] = useSpring(() => ({ + const [isEditVisible, setEditVisible] = useState(false); + + const [{ x, replyOpacity, iconScale }, api] = useSpring(() => ({ x: 0, replyOpacity: 0, - editOpacity: 0, iconScale: 0.5, config: { tension: 250, friction: 25 }, })); const bind = useDrag( - ({ down, movement: [mvx], vxvy: [vx] }) => { + ({ down, movement: [mvx] }) => { if (!down) { const finalDistance = Math.abs(mvx); @@ -49,19 +50,18 @@ export function DraggableMessage({ const xTarget = down ? Math.min(0, mvx) : 0; const distance = Math.abs(xTarget); + setEditVisible(canEdit && distance >= EDIT_THRESHOLD); + let newReplyOpacity = 0; - let newEditOpacity = 0; let newScale = 1.0; - if (canEdit && distance > REPLY_THRESHOLD) { + if (canEdit && (distance < REPLY_THRESHOLD || distance >= EDIT_THRESHOLD)) { newReplyOpacity = 0; - newEditOpacity = 1; if (down && distance > EDIT_THRESHOLD) { newScale = 1.1; } } else { newReplyOpacity = 1; - newEditOpacity = 0; newScale = 0.5 + (distance / REPLY_THRESHOLD) * 0.5; } @@ -72,7 +72,6 @@ export function DraggableMessage({ api.start({ x: xTarget, replyOpacity: newReplyOpacity, - editOpacity: newEditOpacity, iconScale: newScale, }); }, @@ -82,6 +81,7 @@ export function DraggableMessage({ threshold: 10, } ); + if (isMobile) { return (
@@ -99,7 +99,7 @@ export function DraggableMessage({ `scale(${s})`), }} > From a6898e1484e8947ba2f7503bb510c9f7207a0f91 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 21:49:32 -0500 Subject: [PATCH 69/73] Fix the crash caused by the emoji board load --- .../room/message/MessageOptionsMenu.tsx | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/app/features/room/message/MessageOptionsMenu.tsx b/src/app/features/room/message/MessageOptionsMenu.tsx index 2fc508bf..c5f85ada 100644 --- a/src/app/features/room/message/MessageOptionsMenu.tsx +++ b/src/app/features/room/message/MessageOptionsMenu.tsx @@ -197,7 +197,6 @@ export function MessageOptionsMenu({ }: ExtendedOptionsProps) { const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; - const [view, setView] = useState('options'); const eventId = mEvent.getId(); if (!eventId) return null; @@ -247,7 +246,7 @@ export function MessageOptionsMenu({ }} isOpen={isEmojiBoardOpen} > - Loading

}> +

}> { - onReactionToggle(eventId, key); - setEmojiBoardAnchor(undefined); - }} - onCustomEmojiSelect={(mxc, shortcode) => { - onReactionToggle(eventId, mxc, shortcode); - setEmojiBoardAnchor(undefined); - }} - requestClose={() => setEmojiBoardAnchor(undefined)} - /> +

}> + { + onReactionToggle(eventId, key); + setEmojiBoardAnchor(undefined); + }} + onCustomEmojiSelect={(mxc, shortcode) => { + onReactionToggle(eventId, mxc, shortcode); + setEmojiBoardAnchor(undefined); + }} + requestClose={() => setEmojiBoardAnchor(undefined)} + /> +
} > Date: Wed, 18 Jun 2025 21:56:28 -0500 Subject: [PATCH 70/73] Add bounds to prevent swiping the other direction for the same effect without visuals --- src/app/components/message/behavior/DraggableMessage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/components/message/behavior/DraggableMessage.tsx b/src/app/components/message/behavior/DraggableMessage.tsx index 31394d4e..3bd035a1 100644 --- a/src/app/components/message/behavior/DraggableMessage.tsx +++ b/src/app/components/message/behavior/DraggableMessage.tsx @@ -47,7 +47,7 @@ export function DraggableMessage({ } } - const xTarget = down ? Math.min(0, mvx) : 0; + const xTarget = down ? mvx : 0; const distance = Math.abs(xTarget); setEditVisible(canEdit && distance >= EDIT_THRESHOLD); @@ -79,6 +79,7 @@ export function DraggableMessage({ axis: 'x', filterTaps: true, threshold: 10, + bounds: { right: 0 }, } ); From 622521ca0aa693300548c8b10fa241d1b9f6d201 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Wed, 18 Jun 2025 21:58:18 -0500 Subject: [PATCH 71/73] bump the reply threshold --- src/app/components/message/behavior/DraggableMessage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/message/behavior/DraggableMessage.tsx b/src/app/components/message/behavior/DraggableMessage.tsx index 3bd035a1..2a1c1588 100644 --- a/src/app/components/message/behavior/DraggableMessage.tsx +++ b/src/app/components/message/behavior/DraggableMessage.tsx @@ -23,7 +23,7 @@ export function DraggableMessage({ const isMobile = screenSize === ScreenSize.Mobile; const canEdit = mx.getUserId() === event.getSender(); - const REPLY_THRESHOLD = 30; + const REPLY_THRESHOLD = 50; const EDIT_THRESHOLD = canEdit ? 180 : Infinity; const [isEditVisible, setEditVisible] = useState(false); @@ -55,7 +55,7 @@ export function DraggableMessage({ let newReplyOpacity = 0; let newScale = 1.0; - if (canEdit && (distance < REPLY_THRESHOLD || distance >= EDIT_THRESHOLD)) { + if (canEdit && (distance <= REPLY_THRESHOLD || distance >= EDIT_THRESHOLD)) { newReplyOpacity = 0; if (down && distance > EDIT_THRESHOLD) { newScale = 1.1; From 4963142ed4d76afb3ea4c79b864edacf1839b982 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 22 Jun 2025 03:35:21 -0500 Subject: [PATCH 72/73] Prevent the desktop context menu from appearing on mobile --- src/app/pages/client/sidebar/SpaceTabs.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index a66a9d41..98f4d600 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -433,10 +433,12 @@ function SpaceTab({ const handleContextMenu: MouseEventHandler = (evt) => { evt.preventDefault(); const cords = evt.currentTarget.getBoundingClientRect(); - setMenuAnchor((currentState) => { - if (currentState) return undefined; - return cords; - }); + if (!isMobile) { + setMenuAnchor((currentState) => { + if (currentState) return undefined; + return cords; + }); + } }; const longPressBinder = useLongPress( From a4c5d1a6f91a27e69ee87dff5a57201adec4be99 Mon Sep 17 00:00:00 2001 From: Gigiaj Date: Sun, 22 Jun 2025 03:35:58 -0500 Subject: [PATCH 73/73] Completely revise the animations for the context menu to be compatible with Firefox for Android (worked everywhere else amusingly) --- .../mobile-context-menu/MobileContextMenu.jsx | 67 +++++++++++-------- .../MobileContextMenu.scss | 39 +++++++---- 2 files changed, 67 insertions(+), 39 deletions(-) diff --git a/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx b/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx index fd809fa0..b9beda87 100644 --- a/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx +++ b/src/app/molecules/mobile-context-menu/MobileContextMenu.jsx @@ -1,10 +1,21 @@ -import React, { useEffect } from 'react'; -import { useSpring, animated } from '@react-spring/web'; +import React, { useState, useEffect } from 'react'; import { useDrag } from 'react-use-gesture'; import './MobileContextMenu.scss'; export function MobileContextMenu({ isOpen, onClose, children }) { - const { innerHeight } = window; + const getInnerHeight = () => (typeof window !== 'undefined' ? window.innerHeight : 0); + const [y, setY] = useState(getInnerHeight()); + const [isDragging, setIsDragging] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => { + setY(isOpen ? 0 : getInnerHeight()); + }, 10); + + return () => clearTimeout(timer); + }, [isOpen]); + + useEffect(() => { if (isOpen) { document.body.style.overscrollBehavior = 'contain'; @@ -14,51 +25,53 @@ export function MobileContextMenu({ isOpen, onClose, children }) { }; }, [isOpen]); - const [{ y }, api] = useSpring(() => ({ - y: innerHeight, - config: { tension: 250, friction: 25 }, - })); - - useEffect(() => { - api.start({ y: isOpen ? 0 : innerHeight }); - }, [api, innerHeight, isOpen]); - const bind = useDrag( - ({ last, movement: [, my], event }) => { + ({ last, movement: [, my], down }) => { + if (down && !isDragging) { + setIsDragging(true); + } + + const newY = Math.max(my, 0); + setY(newY); + if (last) { - if (my > innerHeight / 4) { - event.preventDefault(); - event.stopPropagation(); + setIsDragging(false); + if (my > getInnerHeight() / 4) { onClose(); } else { - api.start({ y: 0 }); + setY(0); } - } else { - api.start({ y: Math.max(my, 0), immediate: true }); } }, { - from: () => [0, y.get()], + from: () => [0, y], filterTaps: true, bounds: { top: 0 }, rubberband: true, } ); - if (!isOpen) return null; + + if (!isOpen && y >= getInnerHeight()) return null; + const containerClasses = [ + 'bottom-sheet-container', + !isDragging ? 'is-transitioning' : '', + ].join(' '); + + const backdropOpacity = y > 0 ? 1 - y / getInnerHeight() : 1; return ( <> - - @@ -66,7 +79,7 @@ export function MobileContextMenu({ isOpen, onClose, children }) {
{children}
-
+
); } diff --git a/src/app/molecules/mobile-context-menu/MobileContextMenu.scss b/src/app/molecules/mobile-context-menu/MobileContextMenu.scss index f42e1f75..326385da 100644 --- a/src/app/molecules/mobile-context-menu/MobileContextMenu.scss +++ b/src/app/molecules/mobile-context-menu/MobileContextMenu.scss @@ -1,34 +1,49 @@ .bottom-sheet-backdrop { position: fixed; - inset: 0; + top: 0; + left: 0; + width: 100%; + height: 100%; background-color: rgba(0, 0, 0, 0.5); - z-index: 1000; - touch-action: none; + // The backdrop fade will also be smoother with a transition + transition: opacity 300ms ease-out; + z-index: 999; } .bottom-sheet-container { position: fixed; - left: 0; - right: 0; bottom: 0; - z-index: 1001; + left: 0; + width: 100%; + // Set transform from the component's style prop + // The 'will-change' property is a performance hint for the browser + will-change: transform; + z-index: 1000; + + // Your existing styles for the sheet itself + border-top-left-radius: 16px; + border-top-right-radius: 16px; + box-shadow: 0 -2px A10px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; - max-height: 90vh; - border-radius: 16px 16px 0 0; - box-shadow: 0px -4px B16px rgba(0, 0, 0, 0.15); + + // This is the magic: apply a transition only when this class is present + &.is-transitioning { + transition: transform 300ms ease-out; + } } +// Your other styles remain the same .bottom-sheet-grabber { - flex-shrink: 0; width: 40px; height: 5px; - border-radius: 2.5px; background-color: #ccc; + border-radius: 2.5px; margin: 8px auto; + cursor: grab; } .bottom-sheet-content { + padding: 16px; overflow-y: auto; - padding: 0 1rem 1rem 1rem; }