From 6f182e2f4ce5c525729e4b76b14674f730f4d318 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Fri, 24 Jul 2026 23:28:31 +0200 Subject: [PATCH 01/43] refactor: extract ModalOverlay and fix create room layout on mobile --- .../leave-room-prompt/LeaveRoomPrompt.tsx | 118 +++++++----------- .../components/modal-overlay/ModalOverlay.tsx | 28 +++++ src/app/features/create-room/CreateRoom.tsx | 1 - .../features/create-room/CreateRoomModal.tsx | 97 ++++++-------- .../client/create-room/CreateRoomPage.tsx | 15 ++- tests/e2e/leave-room-prompt.spec.ts | 22 ++++ 6 files changed, 140 insertions(+), 141 deletions(-) create mode 100644 src/app/components/modal-overlay/ModalOverlay.tsx create mode 100644 tests/e2e/leave-room-prompt.spec.ts diff --git a/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx b/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx index 48f11d0e8a..9a457f5a76 100644 --- a/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx +++ b/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx @@ -1,24 +1,10 @@ import { useCallback, useEffect } from 'react'; -import FocusTrap from 'focus-trap-react'; -import { - Dialog, - Overlay, - OverlayCenter, - OverlayBackdrop, - Header, - config, - Box, - Text, - IconButton, - color, - Button, - Spinner, -} from 'folds'; +import { Dialog, Header, config, Box, Text, IconButton, color, Button, Spinner } from 'folds'; import type { MatrixError } from '$types/matrix-sdk'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { composerIcon, X } from '$components/icons/phosphor'; -import { stopPropagation } from '$utils/keyboard'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; import { createDebugLogger } from '$utils/debugLogger'; const debugLog = createDebugLogger('LeaveRoomPrompt'); @@ -50,63 +36,51 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro }, [leaveState, onDone, roomId]); return ( - }> - - + +
- -
- - Leave Room - - - {composerIcon(X)} - -
- - - Are you sure you want to leave this room? - {leaveState.status === AsyncStatus.Error && ( - - Failed to leave room! {leaveState.error.message} - - )} - - - -
- - - + + Leave Room + + + {composerIcon(X)} + +
+ + + Are you sure you want to leave this room? + {leaveState.status === AsyncStatus.Error && ( + + Failed to leave room! {leaveState.error.message} + + )} + + + +
+ ); } diff --git a/src/app/components/modal-overlay/ModalOverlay.tsx b/src/app/components/modal-overlay/ModalOverlay.tsx new file mode 100644 index 0000000000..3915a97f5f --- /dev/null +++ b/src/app/components/modal-overlay/ModalOverlay.tsx @@ -0,0 +1,28 @@ +import type { ReactNode } from 'react'; +import FocusTrap from 'focus-trap-react'; +import { Overlay, OverlayBackdrop, OverlayCenter } from 'folds'; +import { stopPropagation } from '$utils/keyboard'; + +type ModalOverlayProps = { + requestClose: () => void; + children: ReactNode; +}; + +export function ModalOverlay({ requestClose, children }: ModalOverlayProps) { + return ( + }> + + + {children} + + + + ); +} diff --git a/src/app/features/create-room/CreateRoom.tsx b/src/app/features/create-room/CreateRoom.tsx index c41378017a..9cb42c524d 100644 --- a/src/app/features/create-room/CreateRoom.tsx +++ b/src/app/features/create-room/CreateRoom.tsx @@ -211,7 +211,6 @@ export function CreateRoomForm({ required before={getCreateRoomAccessToIcon(access, type, '100')} name="nameInput" - autoFocus size="500" variant="SurfaceVariant" radii="400" diff --git a/src/app/features/create-room/CreateRoomModal.tsx b/src/app/features/create-room/CreateRoomModal.tsx index b5e7026d06..f8ec2c0ec0 100644 --- a/src/app/features/create-room/CreateRoomModal.tsx +++ b/src/app/features/create-room/CreateRoomModal.tsx @@ -1,22 +1,10 @@ -import { - Box, - config, - Header, - IconButton, - Modal, - Overlay, - OverlayBackdrop, - OverlayCenter, - Scroll, - Text, -} from 'folds'; +import { Box, config, Header, IconButton, Modal, Scroll, Text } from 'folds'; import { X, composerIcon } from '$components/icons/phosphor'; -import FocusTrap from 'focus-trap-react'; import { useAllJoinedRoomsSet, useGetRoom } from '$hooks/useGetRoom'; import { SpaceProvider } from '$hooks/useSpace'; import { useCloseCreateRoomModal, useCreateRoomModalState } from '$state/hooks/createRoomModal'; import type { CreateRoomModalState } from '$state/createRoomModal'; -import { stopPropagation } from '$utils/keyboard'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; import { CreateRoomType } from '$components/create-room/types'; import { CreateRoomForm } from './CreateRoom'; @@ -33,53 +21,42 @@ function CreateRoomModal({ state }: CreateRoomModalProps) { return ( - }> - - - - -
- - - {type === CreateRoomType.VoiceRoom ? 'New Voice Room' : 'New Chat Room'} - - - - - {composerIcon(X)} - - -
- - - - - + + + +
+ + + {type === CreateRoomType.VoiceRoom ? 'New Voice Room' : 'New Chat Room'} + - - - - + + + {composerIcon(X)} + + +
+ + + + + +
+
+
); } diff --git a/src/app/pages/client/create-room/CreateRoomPage.tsx b/src/app/pages/client/create-room/CreateRoomPage.tsx index 75a1f22fd7..ebd89b2c8a 100644 --- a/src/app/pages/client/create-room/CreateRoomPage.tsx +++ b/src/app/pages/client/create-room/CreateRoomPage.tsx @@ -98,17 +98,16 @@ export function CreateRoomPage() { )} - + {isMobile && ( - - - - {title} - - - + + + + {title} + + backNavigate(-1)} diff --git a/tests/e2e/leave-room-prompt.spec.ts b/tests/e2e/leave-room-prompt.spec.ts new file mode 100644 index 0000000000..efe856dcb9 --- /dev/null +++ b/tests/e2e/leave-room-prompt.spec.ts @@ -0,0 +1,22 @@ +import { test, expect, type Page } from '@playwright/test'; + +async function dismissDeviceBanner(page: Page): Promise { + await page + .getByRole('button', { name: 'Dismiss' }) + .click({ timeout: 5_000 }) + .catch(() => undefined); +} + +test('leave room prompt opens from the room options menu', async ({ page }) => { + await page.goto('/'); + const room = page.getByText('General').first(); + await expect(room).toBeVisible({ timeout: 180_000 }); + await dismissDeviceBanner(page); + + await room.hover(); + await page.getByRole('button', { name: 'More Options' }).first().click(); + // The mobile sheet can overflow the viewport, where a real pointer click fails. + await page.getByRole('button', { name: 'Leave Room' }).dispatchEvent('click'); + + await expect(page.getByText('Are you sure you want to leave this room?')).toBeVisible(); +}); From 0de7143be96c500082172df3966f6d0cf40b19e8 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Sat, 25 Jul 2026 00:08:43 +0200 Subject: [PATCH 02/43] refactor: migrate remaining matching modals to ModalOverlay --- .../DirectInvitePrompt.tsx | 134 +++---- .../join-address-prompt/JoinAddressPrompt.tsx | 133 +++--- .../knock-room-prompt/KnockRoomPrompt.tsx | 132 +++--- .../leave-space-prompt/LeaveSpacePrompt.tsx | 157 +++----- src/app/components/message/PollEvent.tsx | 50 +-- src/app/components/theme/CssViewerButton.tsx | 36 +- src/app/components/user-profile/PowerChip.tsx | 138 +++---- src/app/components/user-profile/UserHero.tsx | 38 +- src/app/features/add-existing/AddExisting.tsx | 379 +++++++++--------- .../features/bug-report/BugReportModal.tsx | 27 +- .../general/RoomEncryption.tsx | 67 ++-- .../common-settings/general/RoomUpgrade.tsx | 143 +++---- .../create-space/CreateSpaceModal.tsx | 95 ++--- .../features/room/jump-to-time/JumpToTime.tsx | 315 +++++++-------- .../room/location-modal/LocationDialog.tsx | 268 ++++++------- .../features/room/poll-modals/PollDialog.tsx | 314 +++++++-------- .../schedule-send/SchedulePickerDialog.tsx | 368 ++++++++--------- src/app/pages/client/sidebar/SpaceTabs.tsx | 109 +++-- src/app/pages/client/space/Space.tsx | 41 +- 19 files changed, 1279 insertions(+), 1665 deletions(-) diff --git a/src/app/components/direct-invite-prompt/DirectInvitePrompt.tsx b/src/app/components/direct-invite-prompt/DirectInvitePrompt.tsx index f17c4d1139..ead5938dfb 100644 --- a/src/app/components/direct-invite-prompt/DirectInvitePrompt.tsx +++ b/src/app/components/direct-invite-prompt/DirectInvitePrompt.tsx @@ -1,20 +1,6 @@ -import FocusTrap from 'focus-trap-react'; -import { - Box, - Button, - Dialog, - Header, - IconButton, - Overlay, - OverlayBackdrop, - OverlayCenter, - Spinner, - Text, - color, - config, -} from 'folds'; +import { Box, Button, Dialog, Header, IconButton, Spinner, Text, color, config } from 'folds'; import { composerIcon, X } from '$components/icons/phosphor'; -import { stopPropagation } from '$utils/keyboard'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type DirectInvitePromptProps = { onCancel: () => void; @@ -32,74 +18,58 @@ export function DirectInvitePrompt({ convertError, }: DirectInvitePromptProps) { return ( - }> - - + +
- -
+ Invite another Member + + + {composerIcon(X)} + +
+ + + + This is a Direct Message room, intended for a conversation between two persons. + Would you like to convert it into a group chat before continuing? + + {convertError && ( + + Failed to convert direct message to room! {convertError} + + )} + + +
- - - - This is a Direct Message room, intended for a conversation between two - persons. Would you like to convert it into a group chat before continuing? - - {convertError && ( - - Failed to convert direct message to room! {convertError} - - )} - - - - - - - -
-
-
-
+ + {converting ? 'Converting...' : 'Convert to Group Chat and Invite'} + + + + +
+
+ + ); } diff --git a/src/app/components/join-address-prompt/JoinAddressPrompt.tsx b/src/app/components/join-address-prompt/JoinAddressPrompt.tsx index 98cf7883cf..f1eefb05a4 100644 --- a/src/app/components/join-address-prompt/JoinAddressPrompt.tsx +++ b/src/app/components/join-address-prompt/JoinAddressPrompt.tsx @@ -1,24 +1,10 @@ import type { FormEventHandler } from 'react'; import { useState } from 'react'; -import FocusTrap from 'focus-trap-react'; -import { - Dialog, - Overlay, - OverlayCenter, - OverlayBackdrop, - Header, - config, - Box, - Text, - IconButton, - Button, - Input, - color, -} from 'folds'; +import { Dialog, Header, config, Box, Text, IconButton, Button, Input, color } from 'folds'; import { composerIcon, X } from '$components/icons/phosphor'; -import { stopPropagation } from '$utils/keyboard'; import { isRoomAlias, isRoomId } from '$utils/matrix'; import { parseMatrixToRoom, parseMatrixToRoomEvent, testMatrixTo } from '$plugins/matrix-to'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type JoinAddressProps = { onOpen: (roomIdOrAlias: string, via?: string[], eventId?: string) => void; @@ -61,71 +47,60 @@ export function JoinAddressPrompt({ onOpen, onCancel }: JoinAddressProps) { }; return ( - }> - - + +
- -
+ Join with Address + + + {composerIcon(X)} + +
+ + + + Enter public address to join the community. Addresses looks like: + + +
  • #community:server
  • +
  • https://matrix.to/#/#community:server
  • +
  • https://matrix.to/#/!xYzAj?via=server
  • +
    +
    + + Address + - - Join with Address - - - {composerIcon(X)} - -
    - - - - Enter public address to join the community. Addresses looks like: - - -
  • #community:server
  • -
  • https://matrix.to/#/#community:server
  • -
  • https://matrix.to/#/!xYzAj?via=server
  • -
    -
    - - Address - - {invalid && ( - - Invalid Address - - )} - - -
    -
    -
    -
    -
    + autoFocus + name="addressInput" + variant="Background" + placeholder="#community:server" + required + /> + {invalid && ( + + Invalid Address + + )} +
    + +
    + + ); } diff --git a/src/app/components/knock-room-prompt/KnockRoomPrompt.tsx b/src/app/components/knock-room-prompt/KnockRoomPrompt.tsx index 36ddcdd0b8..c31a027048 100644 --- a/src/app/components/knock-room-prompt/KnockRoomPrompt.tsx +++ b/src/app/components/knock-room-prompt/KnockRoomPrompt.tsx @@ -1,11 +1,7 @@ import type { FormEventHandler } from 'react'; import { useCallback, useEffect } from 'react'; -import FocusTrap from 'focus-trap-react'; import { Dialog, - Overlay, - OverlayCenter, - OverlayBackdrop, Header, config, Box, @@ -21,8 +17,8 @@ import type { MatrixError } from '$types/matrix-sdk'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { composerIcon, X } from '$components/icons/phosphor'; -import { stopPropagation } from '$utils/keyboard'; import { createDebugLogger } from '$utils/debugLogger'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; const debugLog = createDebugLogger('KnockRoomPrompt'); @@ -61,79 +57,67 @@ export function KnockRoomPrompt({ roomId, via, onDone, onCancel }: KnockRoomProp }, [knockState, onDone, roomId]); return ( - }> - - + +
    - -
    - - Knock On Room - - - {composerIcon(X)} - -
    - - - - Request to join this room. You can optionally leave a reason for the moderators. + + Knock On Room + + + {composerIcon(X)} + +
    + + + + Request to join this room. You can optionally leave a reason for the moderators. + + + + Reason{' '} + + (Optional) - - - Reason{' '} - - (Optional) - - - - {knockState.status === AsyncStatus.Error && ( - - Failed to knock! {knockState.error.message} - - )} - - - + )} -
    -
    -
    -
    +
    + +
    + + ); } diff --git a/src/app/components/leave-space-prompt/LeaveSpacePrompt.tsx b/src/app/components/leave-space-prompt/LeaveSpacePrompt.tsx index f4942122ee..ecec11fa25 100644 --- a/src/app/components/leave-space-prompt/LeaveSpacePrompt.tsx +++ b/src/app/components/leave-space-prompt/LeaveSpacePrompt.tsx @@ -1,26 +1,12 @@ import { useCallback, useEffect, useMemo } from 'react'; -import FocusTrap from 'focus-trap-react'; -import { - Dialog, - Overlay, - OverlayCenter, - OverlayBackdrop, - Header, - config, - Box, - Text, - IconButton, - color, - Button, - Spinner, -} from 'folds'; +import { Dialog, Header, config, Box, Text, IconButton, color, Button, Spinner } from 'folds'; import type { MatrixError } from '$types/matrix-sdk'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { composerIcon, X } from '$components/icons/phosphor'; -import { stopPropagation } from '$utils/keyboard'; import { getJoinedSpaceChildrenSummary, getRecursiveSpaceLeaveOrder } from '$utils/room'; import { rateLimitedActions } from '$utils/matrix'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type LeaveSpacePromptProps = { roomId: string; @@ -91,85 +77,70 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP }, [leaveState, leaveAllState, onDone]); return ( - }> - - + +
    - -
    + Leave Space + + + {composerIcon(X)} + +
    + + + Are you sure you want to leave this space? + {joinedChildrenCount > 0 && ( + + {formatJoinedContentsMessage(roomCount, subspaceCount)} + + )} + {leaveState.status === AsyncStatus.Error && ( + + Failed to leave space! {leaveState.error.message} + + )} + {leaveAllState.status === AsyncStatus.Error && ( + + Failed to leave space, rooms, and subspaces! {leaveAllState.error.message} + + )} + + +
    - - - Are you sure you want to leave this space? - {joinedChildrenCount > 0 && ( - - {formatJoinedContentsMessage(roomCount, subspaceCount)} - - )} - {leaveState.status === AsyncStatus.Error && ( - - Failed to leave space! {leaveState.error.message} - - )} - {leaveAllState.status === AsyncStatus.Error && ( - - Failed to leave space, rooms, and subspaces! {leaveAllState.error.message} - - )} - - - - {joinedChildrenCount > 0 && ( - - )} - - -
    -
    -
    -
    + {leaving ? 'Leaving...' : 'Leave Space Only'} + + {joinedChildrenCount > 0 && ( + + )} +
    + + + ); } diff --git a/src/app/components/message/PollEvent.tsx b/src/app/components/message/PollEvent.tsx index bfb1e12104..fa3f6abb1d 100644 --- a/src/app/components/message/PollEvent.tsx +++ b/src/app/components/message/PollEvent.tsx @@ -1,16 +1,4 @@ -import { - Box, - Button, - Checkbox, - Line, - Modal, - Overlay, - OverlayBackdrop, - OverlayCenter, - ProgressBar, - RadioButton, - Text, -} from 'folds'; +import { Box, Button, Checkbox, Line, Modal, ProgressBar, RadioButton, Text } from 'folds'; import type { MatrixClient, PollStartSubtype, Room, TimelineEvents } from 'matrix-js-sdk'; import { M_TEXT } from 'matrix-js-sdk'; import { @@ -26,8 +14,7 @@ import { import * as css from './PollEvent.css'; import { useCallback, useEffect, useState } from 'react'; import { PollResponsesViewer } from '$features/room/poll-modals'; -import { stopPropagation } from '$utils/keyboard'; -import FocusTrap from 'focus-trap-react'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type PollEventProps = { content: Record; @@ -346,28 +333,17 @@ export function PollEvent({ content, mEvent, mx, room }: PollEventProps) { {ViewVotersAnswer && ( - }> - - setViewVotersAnswer(undefined), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, - }} - > - - setViewVotersAnswer(undefined)} - /> - - - - + setViewVotersAnswer(undefined)}> + + setViewVotersAnswer(undefined)} + /> + + )} ); diff --git a/src/app/components/theme/CssViewerButton.tsx b/src/app/components/theme/CssViewerButton.tsx index fff20ddaec..e51ebd6d71 100644 --- a/src/app/components/theme/CssViewerButton.tsx +++ b/src/app/components/theme/CssViewerButton.tsx @@ -1,11 +1,10 @@ import { useState } from 'react'; -import FocusTrap from 'focus-trap-react'; -import { IconButton, Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds'; +import { IconButton, Modal } from 'folds'; import { Code, sizedIcon } from '$components/icons/phosphor'; import { TextViewer } from '$components/text-viewer'; import { ModalWide } from '$styles/Modal.css'; -import { stopPropagation } from '$utils/keyboard'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type CssViewerButtonProps = { title: string; @@ -56,27 +55,16 @@ export function CssViewerButton({ title, cssText, loadCssText, ariaLabel }: CssV {sizedIcon(Code, '200')} {open && ( - }> - - setOpen(false), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, - }} - > - - setOpen(false)} - /> - - - - + setOpen(false)}> + + setOpen(false)} + /> + + )} ); diff --git a/src/app/components/user-profile/PowerChip.tsx b/src/app/components/user-profile/PowerChip.tsx index 2ee1504537..c490a8e55d 100644 --- a/src/app/components/user-profile/PowerChip.tsx +++ b/src/app/components/user-profile/PowerChip.tsx @@ -10,9 +10,6 @@ import { Line, Menu, MenuItem, - Overlay, - OverlayBackdrop, - OverlayCenter, PopOut, Spinner, Text, @@ -47,6 +44,7 @@ import { PowerColorBadge, PowerIcon } from '$components/power'; import { EventType } from '$types/matrix-sdk'; import { heroMenuItemStyle } from './heroMenuItemStyle'; import * as css from './styles.css'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type SelfDemoteAlertProps = { power: number; @@ -55,46 +53,35 @@ type SelfDemoteAlertProps = { }; function SelfDemoteAlert({ power, onCancel, onChange }: SelfDemoteAlertProps) { return ( - }> - - + +
    - -
    - - Self Demotion - - - {menuIcon(X)} - -
    - - - - You are about to demote yourself! You will not be able to regain this power - yourself. Are you sure? - - - - - - -
    - - - + + Self Demotion + + + {menuIcon(X)} + +
    + + + + You are about to demote yourself! You will not be able to regain this power yourself. + Are you sure? + + + + + + +
    + ); } @@ -105,46 +92,35 @@ type SharedPowerAlertProps = { }; function SharedPowerAlert({ power, onCancel, onChange }: SharedPowerAlertProps) { return ( - }> - - + +
    - -
    - - Shared Power - - - {menuIcon(X)} - -
    - - - - You are promoting the user to have the same power as yourself! You will not be - able to change their power afterward. Are you sure? - - - - - - -
    - - - + + Shared Power + + + {menuIcon(X)} + +
    + + + + You are promoting the user to have the same power as yourself! You will not be able to + change their power afterward. Are you sure? + + + + + + +
    + ); } diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index a41b5332f9..9d82236019 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -5,9 +5,6 @@ import { Box, color as standardColors, Modal, - Overlay, - OverlayBackdrop, - OverlayCenter, Scroll, Text, Tooltip, @@ -16,12 +13,10 @@ import { config, } from 'folds'; import classNames from 'classnames'; -import FocusTrap from 'focus-trap-react'; import colorMXID from '$utils/colorMXID'; import { getMxIdLocalPart } from '$utils/matrix'; import { BreakWord, LineClamp3 } from '$styles/Text.css'; import type { UserPresence } from '$hooks/useUserPresence'; -import { stopPropagation } from '$utils/keyboard'; import { useRoom } from '$hooks/useRoom'; import { useSableCosmetics } from '$hooks/useSableCosmetics'; import { useNickname } from '$hooks/useNickname'; @@ -44,6 +39,7 @@ import { copyToClipboard } from '$utils/dom'; import { useTimeoutToggle } from '$hooks/useTimeoutToggle'; import { CopyIcon, CrossIcon } from '@phosphor-icons/react'; import { useOpenSettings } from '$features/settings'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type UserHeroProps = { userId: string; @@ -156,29 +152,15 @@ export function UserHero({ {viewAvatar && ( - }> - - setViewAvatar(undefined), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, - }} - > - evt.stopPropagation()} - > - setViewAvatar(undefined)} - /> - - - - + setViewAvatar(undefined)}> + evt.stopPropagation()}> + setViewAvatar(undefined)} + /> + + )} {((status && status.length > 0) || allowEditing) && ( diff --git a/src/app/features/add-existing/AddExisting.tsx b/src/app/features/add-existing/AddExisting.tsx index ca743979b7..b0876fba10 100644 --- a/src/app/features/add-existing/AddExisting.tsx +++ b/src/app/features/add-existing/AddExisting.tsx @@ -1,4 +1,3 @@ -import FocusTrap from 'focus-trap-react'; import { Avatar, Box, @@ -10,9 +9,6 @@ import { Menu, MenuItem, Modal, - Overlay, - OverlayBackdrop, - OverlayCenter, Scroll, Spinner, Text, @@ -30,7 +26,6 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { useAtomValue } from 'jotai'; import { useVirtualizer } from '@tanstack/react-virtual'; import type { Room, StateEvents } from '$types/matrix-sdk'; -import { stopPropagation } from '$utils/keyboard'; import { useDirects, useRooms, useSpaces } from '$state/hooks/roomList'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { allRoomsAtom } from '$state/room-list/roomList'; @@ -52,6 +47,7 @@ import { getViaServers } from '$plugins/via-servers'; import { rateLimitedActions } from '$utils/matrix'; import { useAlive } from '$hooks/useAlive'; import { EventType } from '$types/matrix-sdk'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; const SEARCH_OPTS: UseAsyncSearchOptions = { limit: 500, @@ -202,207 +198,194 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM }; return ( - }> - - - - -
    + + +
    + + Add Existing + + + + {composerIcon(X)} + + +
    + + + - - Add Existing - - - - {composerIcon(X)} - + + -
    - - + {vItems.length === 0 && ( - - - - {vItems.length === 0 && ( - - - {searchResult ? 'No Match Found' : `No ${space ? 'Spaces' : 'Rooms'}`} - - - {searchResult - ? `No match found for "${searchResult.query}".` - : `You do not have any ${space ? 'Spaces' : 'Rooms'} to display yet.`} - - - )} - - {vItems.map((vItem) => { - const roomId = items[vItem.index]; - if (!roomId) return null; - const room = getRoom(roomId); - if (!room) return null; - const selectedItem = selected?.includes(roomId); - const dm = mDirects.has(room.roomId); + + {searchResult ? 'No Match Found' : `No ${space ? 'Spaces' : 'Rooms'}`} + + + {searchResult + ? `No match found for "${searchResult.query}".` + : `You do not have any ${space ? 'Spaces' : 'Rooms'} to display yet.`} + + + )} + + {vItems.map((vItem) => { + const roomId = items[vItem.index]; + if (!roomId) return null; + const room = getRoom(roomId); + if (!room) return null; + const selectedItem = selected?.includes(roomId); + const dm = mDirects.has(room.roomId); - return ( - - - {dm || room.isSpaceRoom() ? ( - ( - - {nameInitials(room.name)} - - )} - /> - ) : ( - - )} - - } - after={selectedItem && sizedIcon(Check, '200')} - > - - - {queryHighlighRegex - ? highlightText(queryHighlighRegex, [room.name]) - : room.name} - - - - - ); - })} - - {selected.length > 0 && ( - - - - {applyState.status === AsyncStatus.Error ? ( - - Failed to apply changes! Please try again. - - ) : ( - - Apply when ready. ({selected.length} Selected) - - )} - - - - + + {dm || room.isSpaceRoom() ? ( + ( + + {nameInitials(room.name)} + + )} + /> + ) : ( + + )} + + } + after={selectedItem && sizedIcon(Check, '200')} + > + + + {queryHighlighRegex + ? highlightText(queryHighlighRegex, [room.name]) + : room.name} + - - - )} - - + + + ); + })} + + {selected.length > 0 && ( + + + + {applyState.status === AsyncStatus.Error ? ( + + Failed to apply changes! Please try again. + + ) : ( + + Apply when ready. ({selected.length} Selected) + + )} + + + + + + + + )}
    - -
    -
    -
    -
    + + + + + ); } diff --git a/src/app/features/bug-report/BugReportModal.tsx b/src/app/features/bug-report/BugReportModal.tsx index 5d4bde2443..7042f0da2f 100644 --- a/src/app/features/bug-report/BugReportModal.tsx +++ b/src/app/features/bug-report/BugReportModal.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from 'react'; -import FocusTrap from 'focus-trap-react'; import { Box, Button, @@ -9,9 +8,6 @@ import { IconButton, Input, Modal, - Overlay, - OverlayBackdrop, - OverlayCenter, Scroll, Spinner, Text, @@ -21,9 +17,9 @@ import { import { ArrowRight, X, chipIcon, composerIcon } from '$components/icons/phosphor'; import * as Sentry from '@sentry/react'; import { useCloseBugReportModal, useBugReportModalOpen } from '$state/hooks/bugReportModal'; -import { stopPropagation } from '$utils/keyboard'; import { getDebugLogger } from '$utils/debugLogger'; import { fetch } from '$utils/fetch'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type ReportType = 'bug' | 'feature'; @@ -499,22 +495,11 @@ function BugReportModal() { const close = useCloseBugReportModal(); return ( - }> - - - - - - - - + + + + + ); } diff --git a/src/app/features/common-settings/general/RoomEncryption.tsx b/src/app/features/common-settings/general/RoomEncryption.tsx index 9da75c6667..326938d9d7 100644 --- a/src/app/features/common-settings/general/RoomEncryption.tsx +++ b/src/app/features/common-settings/general/RoomEncryption.tsx @@ -7,16 +7,12 @@ import { Dialog, Header, IconButton, - Overlay, - OverlayBackdrop, - OverlayCenter, Spinner, Text, } from 'folds'; import { composerIcon, X } from '$components/icons/phosphor'; import { useCallback, useState } from 'react'; import type { MatrixError, StateEvents } from '$types/matrix-sdk'; -import FocusTrap from 'focus-trap-react'; import { SequenceCard } from '$components/sequence-card'; import { SequenceCardStyle } from '$features/room-settings/styles.css'; import { SettingTile } from '$components/setting-tile'; @@ -25,9 +21,9 @@ import { useMatrixClient } from '$hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { useRoom } from '$hooks/useRoom'; import { useStateEvent } from '$hooks/useStateEvent'; -import { stopPropagation } from '$utils/keyboard'; import type { RoomPermissionsAPI } from '$hooks/useRoomPermissions'; import { EventType } from '$types/matrix-sdk'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; const ROOM_ENC_ALGO = 'm.megolm.v1.aes-sha2'; @@ -101,44 +97,33 @@ export function RoomEncryption({ permissions }: RoomEncryptionProps) { )} {prompt && ( - }> - - setPrompt(false), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, + setPrompt(false)}> + +
    - -
    - - Enable Encryption - - setPrompt(false)} radii="300"> - {composerIcon(X)} - -
    - - - Are you sure? Once enabled, encryption cannot be disabled! - - - -
    - - - + + Enable Encryption + + setPrompt(false)} radii="300"> + {composerIcon(X)} + +
    + + + Are you sure? Once enabled, encryption cannot be disabled! + + + +
    +
    )} diff --git a/src/app/features/common-settings/general/RoomUpgrade.tsx b/src/app/features/common-settings/general/RoomUpgrade.tsx index 6c8b8eb731..b2f8ae9f6f 100644 --- a/src/app/features/common-settings/general/RoomUpgrade.tsx +++ b/src/app/features/common-settings/general/RoomUpgrade.tsx @@ -1,20 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; -import { - Button, - color, - Spinner, - Text, - Overlay, - OverlayBackdrop, - OverlayCenter, - Dialog, - Header, - config, - Box, - IconButton, -} from 'folds'; +import { Button, color, Spinner, Text, Dialog, Header, config, Box, IconButton } from 'folds'; import { composerIcon, X } from '$components/icons/phosphor'; -import FocusTrap from 'focus-trap-react'; import type { MatrixError, RoomTombstoneEventContent } from '$types/matrix-sdk'; import { Method, EventType } from '$types/matrix-sdk'; import { SequenceCard } from '$components/sequence-card'; @@ -28,7 +14,6 @@ import { useMatrixClient } from '$hooks/useMatrixClient'; import { useStateEvent } from '$hooks/useStateEvent'; import { useRoomNavigate } from '$hooks/useRoomNavigate'; import { useCapabilities } from '$hooks/useCapabilities'; -import { stopPropagation } from '$utils/keyboard'; import type { RoomPermissionsAPI } from '$hooks/useRoomPermissions'; import { AdditionalCreatorInput, @@ -39,6 +24,7 @@ import { useAlive } from '$hooks/useAlive'; import { useRoomCreators } from '$hooks/useRoomCreators'; import { BreakWord } from '$styles/Text.css'; import { creatorsSupported } from '$utils/roomSupport'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) { const mx = useMatrixClient(); @@ -83,78 +69,67 @@ function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) { }; return ( - }> - - + +
    - -
    - - {room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'} - - - {composerIcon(X)} - -
    - - - This action is irreversible! - - - Options - + {room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'} + + + {composerIcon(X)} + +
    + + + This action is irreversible! + + + Options + + {allowAdditionalCreators && ( + + - {allowAdditionalCreators && ( - - - - )} - - {upgradeState.status === AsyncStatus.Error && ( - - {(upgradeState.error as MatrixError).message} - - )} - - -
    -
    -
    -
    + + )} + + {upgradeState.status === AsyncStatus.Error && ( + + {(upgradeState.error as MatrixError).message} + + )} + + + + ); } diff --git a/src/app/features/create-space/CreateSpaceModal.tsx b/src/app/features/create-space/CreateSpaceModal.tsx index d3d1ef54ed..139ea04023 100644 --- a/src/app/features/create-space/CreateSpaceModal.tsx +++ b/src/app/features/create-space/CreateSpaceModal.tsx @@ -1,23 +1,11 @@ -import { - Box, - config, - Header, - IconButton, - Modal, - Overlay, - OverlayBackdrop, - OverlayCenter, - Scroll, - Text, -} from 'folds'; +import { Box, config, Header, IconButton, Modal, Scroll, Text } from 'folds'; import { composerIcon, X } from '$components/icons/phosphor'; -import FocusTrap from 'focus-trap-react'; import { useAllJoinedRoomsSet, useGetRoom } from '$hooks/useGetRoom'; import { SpaceProvider } from '$hooks/useSpace'; import { useCloseCreateSpaceModal, useCreateSpaceModalState } from '$state/hooks/createSpaceModal'; import type { CreateSpaceModalState } from '$state/createSpaceModal'; -import { stopPropagation } from '$utils/keyboard'; import { CreateSpaceForm } from './CreateSpace'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type CreateSpaceModalProps = { state: CreateSpaceModalState; @@ -32,52 +20,41 @@ function CreateSpaceModal({ state }: CreateSpaceModalProps) { return ( - }> - - - - -
    - - New Space - - - - {composerIcon(X)} - - -
    - - - - - + + + +
    + + New Space - - - - + + + {composerIcon(X)} + + +
    + + + + + +
    +
    +
    ); } diff --git a/src/app/features/room/jump-to-time/JumpToTime.tsx b/src/app/features/room/jump-to-time/JumpToTime.tsx index 0cf34fa1ec..4fdde66410 100644 --- a/src/app/features/room/jump-to-time/JumpToTime.tsx +++ b/src/app/features/room/jump-to-time/JumpToTime.tsx @@ -5,9 +5,6 @@ import { CaretDown, chipIcon, composerIcon, X } from '$components/icons/phosphor import type { RectCords } from 'folds'; import { Dialog, - Overlay, - OverlayCenter, - OverlayBackdrop, Header, config, Box, @@ -32,6 +29,7 @@ import { getToday, getYesterday, timeDayMonthYear, timeHourMinute } from '$utils import { DatePicker, TimePicker } from '$components/time-date'; import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type JumpToTimeProps = { onCancel: () => void; @@ -87,175 +85,164 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) { }; return ( - }> - - + +
    - -
    - - Jump to Time - - - {composerIcon(X)} - -
    - - - - - Time - - - - {timeHourMinute(ts, hour24Clock)} - - setTimePickerCords(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt: KeyboardEvent) => - evt.key === 'ArrowDown' || evt.key === 'ArrowRight', - isKeyBackward: (evt: KeyboardEvent) => - evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', - escapeDeactivates: stopPropagation, - }} - > - - - } - /> - - - - - Date - - - + Jump to Time + + + {composerIcon(X)} + +
    + + + + + Time + + + + {timeHourMinute(ts, hour24Clock)} + + setTimePickerCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => + evt.key === 'ArrowDown' || evt.key === 'ArrowRight', + isKeyBackward: (evt: KeyboardEvent) => + evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', + escapeDeactivates: stopPropagation, + }} > - {timeDayMonthYear(ts)} - - setDatePickerCords(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt: KeyboardEvent) => - evt.key === 'ArrowDown' || evt.key === 'ArrowRight', - isKeyBackward: (evt: KeyboardEvent) => - evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', - escapeDeactivates: stopPropagation, - }} - > - -
    - } - /> - - + +
    + } + /> - - Preset - - {createTs < todayTs && ( - - Today - - )} - {createTs < yesterdayTs && ( - + + + Date + + + + {timeDayMonthYear(ts)} + + setDatePickerCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => + evt.key === 'ArrowDown' || evt.key === 'ArrowRight', + isKeyBackward: (evt: KeyboardEvent) => + evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', + escapeDeactivates: stopPropagation, + }} > - Yesterday - - )} - - Beginning - - + +
    + } + /> - {timestampState.status === AsyncStatus.Error && ( - - {timestampState.error.message} - + + + + Preset + + {createTs < todayTs && ( + + Today + + )} + {createTs < yesterdayTs && ( + + Yesterday + )} - + Beginning + - -
    -
    -
    + + {timestampState.status === AsyncStatus.Error && ( + + {timestampState.error.message} + + )} + + + + ); } diff --git a/src/app/features/room/location-modal/LocationDialog.tsx b/src/app/features/room/location-modal/LocationDialog.tsx index 9241066b1a..c675fda3bd 100644 --- a/src/app/features/room/location-modal/LocationDialog.tsx +++ b/src/app/features/room/location-modal/LocationDialog.tsx @@ -1,20 +1,6 @@ -import FocusTrap from 'focus-trap-react'; -import { - Dialog, - Overlay, - OverlayCenter, - OverlayBackdrop, - Header, - Box, - Text, - IconButton, - Button, - Input, - Chip, -} from 'folds'; +import { Dialog, Header, Box, Text, IconButton, Button, Input, Chip } from 'folds'; import { ClipboardIcon, MapPinAreaIcon, MapPinLineIcon } from '@phosphor-icons/react'; import { chipIcon, composerIcon, Warning, X } from '$components/icons/phosphor'; -import { stopPropagation } from '$utils/keyboard'; import { readClipboardText } from '$utils/dom'; import type { IContent, MatrixClient, Room } from 'matrix-js-sdk'; import * as css from './LocationDialog.css'; @@ -31,6 +17,7 @@ import { useSetting } from '$state/hooks/settings'; import classNames from 'classnames'; import markerIconPng from 'leaflet/dist/images/marker-icon.png'; import { Icon } from 'leaflet'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; export const markerIcon = new Icon({ iconUrl: markerIconPng, @@ -265,141 +252,130 @@ export function LocationDialog({ }; return ( - }> - - - -
    - - - {`Share Location ${replyDraft ? '(reply / thread)' : ''}`} - - + +
    + + + {`Share Location ${replyDraft ? '(reply / thread)' : ''}`} + + + {composerIcon(X)} + +
    + + + } + > + Share Current Location + + } + > + Paste Clipboard + + + {locationError !== LocationErrors.none && ( + + {chipIcon(Warning)} + {locationError} + + )} + + + Latitude + + + + Longitude + + + + {showMaps && ( + + - {composerIcon(X)} -
    -
    - - - } - > - Share Current Location - - } - > - Paste Clipboard - - - {locationError !== LocationErrors.none && ( - - {chipIcon(Warning)} - {locationError} - - )} - - - Latitude - - - - Longitude - - - - {showMaps && ( - - - - { - e.originalEvent.preventDefault(); - e.originalEvent.stopPropagation(); - }, - }} - icon={markerIcon} - /> + + { + e.originalEvent.preventDefault(); + e.originalEvent.stopPropagation(); + }, + }} + icon={markerIcon} + /> - - - - )} - + + -
    -
    -
    -
    + )} + + + + ); } diff --git a/src/app/features/room/poll-modals/PollDialog.tsx b/src/app/features/room/poll-modals/PollDialog.tsx index bdaa90128c..b4a48c851a 100644 --- a/src/app/features/room/poll-modals/PollDialog.tsx +++ b/src/app/features/room/poll-modals/PollDialog.tsx @@ -1,9 +1,5 @@ -import FocusTrap from 'focus-trap-react'; import { Dialog, - Overlay, - OverlayCenter, - OverlayBackdrop, Header, Box, Text, @@ -16,7 +12,6 @@ import { Scroll, } from 'folds'; import { chipIcon, composerIcon, ListBullets, Minus, X } from '$components/icons/phosphor'; -import { stopPropagation } from '$utils/keyboard'; import type { ChangeEventHandler, KeyboardEventHandler } from 'react'; import { useCallback, useRef, useState } from 'react'; import type { PollAnswerItem } from '$components/message/PollEvent'; @@ -35,6 +30,7 @@ import { isKeyHotkey } from 'is-hotkey'; import * as css from './PollDialog.css'; import type { IReplyDraft } from '$state/room/roomInputDrafts'; import { getReplyContent } from '../RoomInput'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type PollDialogProps = { onCancel: () => void; @@ -154,179 +150,157 @@ export function PollDialog({ onCancel, mx, room, replyDraft, clearReplyDraft }: } }; return ( - }> - - - -
    - - {composerIcon(ListBullets)} - {`New Poll ${replyDraft ? '(reply / thread)' : ''}`} - - + +
    + + {composerIcon(ListBullets)} + {`New Poll ${replyDraft ? '(reply / thread)' : ''}`} + + + {composerIcon(X)} + +
    + + + Title + (title.current = evt.currentTarget.value.trim())} + placeholder={'What should we have for dinner?'} + /> + + + + Options ({answers.length}) + - {composerIcon(X)} -
    -
    - - - Title - (title.current = evt.currentTarget.value.trim())} - placeholder={'What should we have for dinner?'} - /> - - - - Options ({answers.length}) - - Add Option - - - - {answers.map((item, index) => ( - - { - let newAnswers = answers; - newAnswers[index] = { - id: answers[index]?.id ?? randomStr(), - [M_TEXT.name]: evt.currentTarget.value.trim() ?? '', - }; - setAnswers(newAnswers); - }} - placeholder={`Type Option ${index + 1}`} - after={ - delOption(item.id)} - > - {chipIcon(Minus)} - - } - /> - - ))} - - - - Add Option + + + + {answers.map((item, index) => ( + - + - - - { + let newAnswers = answers; + newAnswers[index] = { + id: answers[index]?.id ?? randomStr(), + [M_TEXT.name]: evt.currentTarget.value.trim() ?? '', + }; + setAnswers(newAnswers); + }} + placeholder={`Type Option ${index + 1}`} after={ - + delOption(item.id)} + > + {chipIcon(Minus)} + } /> - + ))} + + + + + } + /> + + + { - const val = Number.parseInt(e.target.value); - if (val) { - setInputValue(val); - setMaxSelections(val); - } - }} - className={css.PollDialogMaxSelectionSlider} + value={inputValue} + onChange={handleMaxOptions} + onKeyDown={handleMaxKeyDown} + outlined /> - - - - {!!error && ( - - {error.errorString} - - )} - -
    -
    -
    -
    + } + /> + { + const val = Number.parseInt(e.target.value); + if (val) { + setInputValue(val); + setMaxSelections(val); + } + }} + className={css.PollDialogMaxSelectionSlider} + /> + + + + {!!error && ( + + {error.errorString} + + )} + + + ); } diff --git a/src/app/features/room/schedule-send/SchedulePickerDialog.tsx b/src/app/features/room/schedule-send/SchedulePickerDialog.tsx index c6609702a9..27aab78305 100644 --- a/src/app/features/room/schedule-send/SchedulePickerDialog.tsx +++ b/src/app/features/room/schedule-send/SchedulePickerDialog.tsx @@ -4,27 +4,14 @@ import { useAtomValue } from 'jotai'; import { CaretDown, chipIcon, composerIcon, X } from '$components/icons/phosphor'; import FocusTrap from 'focus-trap-react'; import type { RectCords } from 'folds'; -import { - Dialog, - Overlay, - OverlayCenter, - OverlayBackdrop, - Header, - config, - Box, - Text, - IconButton, - color, - Button, - Chip, - PopOut, -} from 'folds'; +import { Dialog, Header, config, Box, Text, IconButton, color, Button, Chip, PopOut } from 'folds'; import { stopPropagation } from '$utils/keyboard'; import { timeDayMonthYear, timeHourMinute, hoursToMs, daysToMs } from '$utils/time'; import { DatePicker, TimePicker } from '$components/time-date'; import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; import { serverMaxDelayMsAtom } from '$state/scheduledMessages'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type SchedulePickerDialogProps = { initialTime?: number; @@ -80,193 +67,182 @@ export function SchedulePickerDialog({ const isPast = ts <= now; return ( - }> - - + +
    - -
    - - Schedule Send - - - {composerIcon(X)} - -
    - - - - - Time - - - - {timeHourMinute(ts, hour24Clock)} - - setTimePickerCords(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt: KeyboardEvent) => - evt.key === 'ArrowDown' || evt.key === 'ArrowRight', - isKeyBackward: (evt: KeyboardEvent) => - evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', - escapeDeactivates: stopPropagation, - }} - > - - - } - /> - - - - - Date - - - + Schedule Send + + + {composerIcon(X)} + +
    + + + + + Time + + + + {timeHourMinute(ts, hour24Clock)} + + setTimePickerCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => + evt.key === 'ArrowDown' || evt.key === 'ArrowRight', + isKeyBackward: (evt: KeyboardEvent) => + evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', + escapeDeactivates: stopPropagation, + }} > - {timeDayMonthYear(ts)} - - setDatePickerCords(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt: KeyboardEvent) => - evt.key === 'ArrowDown' || evt.key === 'ArrowRight', - isKeyBackward: (evt: KeyboardEvent) => - evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', - escapeDeactivates: stopPropagation, - }} - > - -
    - } - /> - - + +
    + } + /> - - Quick Schedule - - handlePreset(hoursToMs(1))} - > - In 1 hour - - handlePreset(hoursToMs(4))} - > - In 4 hours - - { - const tomorrow = new Date(); - tomorrow.setDate(tomorrow.getDate() + 1); - tomorrow.setHours(9, 0, 0, 0); - setTs(tomorrow.getTime()); - setError(undefined); - }} - > - Tomorrow 9 AM - - { - const tomorrow = new Date(); - tomorrow.setDate(tomorrow.getDate() + 1); - tomorrow.setHours(14, 0, 0, 0); - setTs(tomorrow.getTime()); - setError(undefined); - }} - > - Tomorrow 2 PM - - + + + + Date + + + + {timeDayMonthYear(ts)} + + setDatePickerCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => + evt.key === 'ArrowDown' || evt.key === 'ArrowRight', + isKeyBackward: (evt: KeyboardEvent) => + evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', + escapeDeactivates: stopPropagation, + }} + > + + + } + /> - {showEncryptionWarning && ( - - Note: This message will be encrypted with current room keys. Devices that join or - are added after scheduling may not be able to decrypt it. - - )} - {(error || isPast) && ( - - {error || 'Selected time is in the past'} - - )} - + Tomorrow 2 PM + - - -
    -
    + + {showEncryptionWarning && ( + + Note: This message will be encrypted with current room keys. Devices that join or are + added after scheduling may not be able to decrypt it. + + )} + {(error || isPast) && ( + + {error || 'Selected time is in the past'} + + )} + + + + ); } diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index a1954644c5..a46709ee05 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -13,9 +13,6 @@ import { Line, Menu, MenuItem, - Overlay, - OverlayBackdrop, - OverlayCenter, PopOut, Text, config, @@ -104,6 +101,7 @@ import { InviteUserPrompt } from '$components/invite-user-prompt'; import { CustomAccountDataEvent } from '$types/matrix/accountData'; import { lastVisitedSpaceIdAtom } from '$state/room/lastSpace'; import { useMobileTapActivation } from '$hooks/useMobileTapActivation'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type SpaceMenuProps = { room: Room; @@ -284,66 +282,55 @@ function RenameFolderDialog({ mx, folder, onClose, onSave }: Readonly}> - - + +
    - -
    - - Rename Folder - - - {composerIcon(X)} - -
    - - - Choose a short label for this folder. Leave empty to show space names again. - - - Folder name - ) => setDraft(e.target.value)} - autoFocus - /> - - - - - - -
    - - - + + Rename Folder + + + {composerIcon(X)} + +
    + + + Choose a short label for this folder. Leave empty to show space names again. + + + Folder name + ) => setDraft(e.target.value)} + autoFocus + /> + + + + + + +
    + ); } diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index f7aca955a0..9e275feaac 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -11,9 +11,6 @@ import { Menu, MenuItem, Modal, - Overlay, - OverlayBackdrop, - OverlayCenter, PopOut, Spinner, Text, @@ -110,6 +107,7 @@ import * as css from './styles.css'; import { isResizingSidebarAtom } from '$state/isResizingSidebar'; import { UserQuickTools } from '../sidebar/UserQuickTools'; import { useRenderableMediaUrl } from '$hooks/useRenderableMediaUrl'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; const debugLog = createDebugLogger('Space'); @@ -424,30 +422,19 @@ function SpaceHeader({ hideText, mx }: { hideText?: boolean; mx: MatrixClient }) )} {hasBanner && bannerViewerOpen && ( - }> - - setBannerViewerOpen(false), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, - }} - > - evt.stopPropagation()} - > - setBannerViewerOpen(false)} - /> - - - - + setBannerViewerOpen(false)}> + evt.stopPropagation()} + > + setBannerViewerOpen(false)} + /> + + )} ); From 19b029ae80df7812d51abfa223054fcfe61bf17a Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Sat, 25 Jul 2026 00:12:16 +0200 Subject: [PATCH 03/43] refactor: support conditional open in ModalOverlay and migrate remaining matching modals --- .../message/content/FileContent.tsx | 97 ++++------- .../message/content/ImageContent.tsx | 47 ++---- .../components/modal-overlay/ModalOverlay.tsx | 5 +- src/app/components/room-card/RoomCard.tsx | 79 +++------ .../common-settings/cosmetics/Cosmetics.tsx | 96 ++++------- .../common-settings/general/RoomProfile.tsx | 63 +++----- src/app/features/lobby/LobbyHero.tsx | 31 ++-- src/app/features/lobby/RoomItem.tsx | 32 ++-- src/app/features/room/RoomViewHeader.tsx | 29 +--- src/app/features/settings/account/Profile.tsx | 153 +++++++----------- src/app/pages/client/explore/Explore.tsx | 109 ++++++------- src/app/pages/client/inbox/Bookmarks.tsx | 83 ++++------ src/app/pages/client/inbox/Invites.tsx | 32 ++-- 13 files changed, 305 insertions(+), 551 deletions(-) diff --git a/src/app/components/message/content/FileContent.tsx b/src/app/components/message/content/FileContent.tsx index ae751be894..53f448b617 100644 --- a/src/app/components/message/content/FileContent.tsx +++ b/src/app/components/message/content/FileContent.tsx @@ -1,21 +1,8 @@ import type { ReactNode } from 'react'; import { useCallback, useState } from 'react'; -import { - Box, - Button, - Modal, - Overlay, - OverlayBackdrop, - OverlayCenter, - Spinner, - Text, - Tooltip, - TooltipProvider, - as, -} from 'folds'; +import { Box, Button, Modal, Spinner, Text, Tooltip, TooltipProvider, as } from 'folds'; import { ArrowRight, Download, sizedIcon, Warning } from '$components/icons/phosphor'; import type { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; -import FocusTrap from 'focus-trap-react'; import type { IFileInfo } from '$types/matrix/common'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { useMatrixClient } from '$hooks/useMatrixClient'; @@ -26,13 +13,13 @@ import { getFileNameExt, mimeTypeToExt, } from '$utils/mimeTypes'; -import { stopPropagation } from '$utils/keyboard'; import { decryptFile, downloadEncryptedMedia, downloadMedia, mxcUrlToHttp } from '$utils/matrix'; import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; import { useRevokeObjectURL } from '$hooks/useObjectURL'; import { useDismissOnBack } from '$utils/androidBack'; import { ModalWide } from '$styles/Modal.css'; import { getDownloadFilename, saveFileToDevice } from '$utils/download'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; const renderErrorButton = (retry: () => void, text: string) => ( {textState.status === AsyncStatus.Success && ( - }> - - setTextViewer(false), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, - }} - > - evt.stopPropagation()} - > - {renderViewer({ - name: body, - text: textState.data, - langName: READABLE_TEXT_MIME_TYPES.includes(mimeType) - ? mimeTypeToExt(mimeType) - : mimeTypeToExt(READABLE_EXT_TO_MIME_TYPE[getFileNameExt(body)] ?? mimeType), - requestClose: () => setTextViewer(false), - })} - - - - + setTextViewer(false)}> + evt.stopPropagation()} + > + {renderViewer({ + name: body, + text: textState.data, + langName: READABLE_TEXT_MIME_TYPES.includes(mimeType) + ? mimeTypeToExt(mimeType) + : mimeTypeToExt(READABLE_EXT_TO_MIME_TYPE[getFileNameExt(body)] ?? mimeType), + requestClose: () => setTextViewer(false), + })} + + )} {textState.status === AsyncStatus.Error ? ( renderErrorButton(loadText, 'Open File') @@ -195,30 +171,19 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read return ( <> {pdfState.status === AsyncStatus.Success && ( - }> - - setPdfViewer(false), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, - }} - > - evt.stopPropagation()} - > - {renderViewer({ - name: body, - src: pdfState.data, - requestClose: () => setPdfViewer(false), - })} - - - - + setPdfViewer(false)}> + evt.stopPropagation()} + > + {renderViewer({ + name: body, + src: pdfState.data, + requestClose: () => setPdfViewer(false), + })} + + )} {pdfState.status === AsyncStatus.Error ? ( renderErrorButton(loadPdf, 'Open PDF') diff --git a/src/app/components/message/content/ImageContent.tsx b/src/app/components/message/content/ImageContent.tsx index cb0325cb64..7d1f2e3ea5 100644 --- a/src/app/components/message/content/ImageContent.tsx +++ b/src/app/components/message/content/ImageContent.tsx @@ -8,9 +8,6 @@ import { Menu, MenuItem, Modal, - Overlay, - OverlayBackdrop, - OverlayCenter, Spinner, Text, Tooltip, @@ -31,14 +28,12 @@ import { } from '$components/icons/phosphor'; import classNames from 'classnames'; import { BlurhashCanvas } from 'react-blurhash'; -import FocusTrap from 'focus-trap-react'; import type { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; import type { IImageInfo } from '$types/matrix/common'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { bytesToSize } from '$utils/common'; import { FALLBACK_MIMETYPE } from '$utils/mimeTypes'; -import { stopPropagation } from '$utils/keyboard'; import { decryptFile, downloadEncryptedMedia, @@ -58,6 +53,7 @@ import { import { useFavoriteGifs } from '$hooks/useFavoriteGifs'; import { useRenderableMediaUrl } from '$hooks/useRenderableMediaUrl'; import { useRevokeObjectURL } from '$hooks/useObjectURL'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; export function checkIfGif(url: string, mimetype?: string, body?: string) { return ( @@ -262,32 +258,21 @@ export const ImageContent = as<'div', ImageContentProps>( onPointerLeave={() => setIsHovered(false)} > {srcState.status === AsyncStatus.Success && ( - }> - - setViewer(false), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, - }} - > - evt.stopPropagation()} - > - {renderViewer({ - src: viewerFullSrc ?? srcState.data, - alt: body ?? '', - filename, - requestClose: () => setViewer(false), - info: info, - })} - - - - + setViewer(false)}> + evt.stopPropagation()} + > + {renderViewer({ + src: viewerFullSrc ?? srcState.data, + alt: body ?? '', + filename, + requestClose: () => setViewer(false), + info: info, + })} + + )} {typeof blurHash === 'string' && !load && ( void; children: ReactNode; }; -export function ModalOverlay({ requestClose, children }: ModalOverlayProps) { +export function ModalOverlay({ open = true, requestClose, children }: ModalOverlayProps) { return ( - }> + }> { @@ -111,32 +96,21 @@ function ErrorDialog({ return ( <> {children(openError)} - }> - - - - - - {title} - - {message} - - - - - - - - + + + + + {title} + + {message} + + + + + + ); } @@ -263,20 +237,9 @@ export const RoomCard = as<'div', RoomCardProps>( {roomTopic} - }> - - - {renderTopicViewer(roomName, roomTopic, closeTopic)} - - - + + {renderTopicViewer(roomName, roomTopic, closeTopic)} + {(roomType === RoomType.Space || joinedRoom?.isSpaceRoom()) && ( Space diff --git a/src/app/features/common-settings/cosmetics/Cosmetics.tsx b/src/app/features/common-settings/cosmetics/Cosmetics.tsx index 462d7eaff9..0843862863 100644 --- a/src/app/features/common-settings/cosmetics/Cosmetics.tsx +++ b/src/app/features/common-settings/cosmetics/Cosmetics.tsx @@ -11,9 +11,6 @@ import { config, Button, Spinner, - OverlayBackdrop, - Overlay, - OverlayCenter, Modal, Dialog, Header, @@ -46,15 +43,14 @@ import type { UploadSuccess } from '$state/upload'; import { createUploadAtom } from '$state/upload'; import { useFilePicker } from '$hooks/useFilePicker'; import { CompactUploadCardRenderer } from '$components/upload-card'; -import FocusTrap from 'focus-trap-react'; import { ImageEditor } from '$components/image-editor'; -import { stopPropagation } from '$utils/keyboard'; import { ModalWide } from '$styles/Modal.css'; import { NameColorEditor } from '$features/settings/account/NameColorEditor'; import { PronounEditor } from '$features/settings/account/PronounEditor'; import type { PronounSet } from '$utils/pronouns'; import { EventType } from '$types/matrix-sdk'; import { CustomStateEvent } from '$types/matrix/room'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; const log = createLogger('Cosmetics'); @@ -171,66 +167,44 @@ export function CosmeticsAvatar({ profile, member, userId, room }: CosmeticsSett )} {imageFileURL && ( - }> - - - - - - - - + + + + + )} - }> - - setAlertRemove(false), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, + setAlertRemove(false)}> + +
    - -
    - - Remove Room Avatar - - setAlertRemove(false)} radii="300"> - {composerIcon(X)} - -
    - - - Are you sure you want to remove room avatar? - - - -
    - - - + + Remove Room Avatar + + setAlertRemove(false)} radii="300"> + {composerIcon(X)} + +
    + + + Are you sure you want to remove room avatar? + + + +
    +
    ); } diff --git a/src/app/features/common-settings/general/RoomProfile.tsx b/src/app/features/common-settings/general/RoomProfile.tsx index ccfbb0e6a1..e6faea5a46 100644 --- a/src/app/features/common-settings/general/RoomProfile.tsx +++ b/src/app/features/common-settings/general/RoomProfile.tsx @@ -9,9 +9,6 @@ import { Header, IconButton, Input, - Overlay, - OverlayBackdrop, - OverlayCenter, Spinner, Text, TextArea, @@ -57,9 +54,8 @@ import { useStateEvent } from '$hooks/useStateEvent'; import type { RoomBannerContent } from '$types/matrix-sdk-events'; import { CustomStateEvent } from '$types/matrix/room'; import { SettingTile } from '$components/setting-tile'; -import { stopPropagation } from '$utils/keyboard'; -import FocusTrap from 'focus-trap-react'; import { reportMediaLoadFailure } from '$utils/mediaLoadDiagnostics'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type RoomProfileEditProps = { canEditAvatar: boolean; @@ -453,42 +449,31 @@ function RoomBannerEdit({ bannerURI, permissions }: Readonly) { )} - }> - - setAlertRemove(false), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, + setAlertRemove(false)}> + +
    - -
    - - Remove Banner - - setAlertRemove(false)} radii="300"> - {composerIcon(X)} - -
    - - Are you sure you want to remove profile banner? - - -
    - - - + + Remove Banner + + setAlertRemove(false)} radii="300"> + {composerIcon(X)} + +
    + + Are you sure you want to remove profile banner? + + +
    +
    ); } diff --git a/src/app/features/lobby/LobbyHero.tsx b/src/app/features/lobby/LobbyHero.tsx index 6fe92839cf..fad26dd065 100644 --- a/src/app/features/lobby/LobbyHero.tsx +++ b/src/app/features/lobby/LobbyHero.tsx @@ -1,5 +1,4 @@ -import { Avatar, Overlay, OverlayBackdrop, OverlayCenter, Text } from 'folds'; -import FocusTrap from 'focus-trap-react'; +import { Avatar, Text } from 'folds'; import { useRoomAvatar, useRoomName, useRoomTopic } from '$hooks/useRoomMeta'; import { useSpace } from '$hooks/useSpace'; import { useMatrixClient } from '$hooks/useMatrixClient'; @@ -8,10 +7,11 @@ import { nameInitials } from '$utils/common'; import { UseStateProvider } from '$components/UseStateProvider'; import { RoomTopicViewer } from '$components/room-topic-viewer'; import { PageHero } from '$components/page'; -import { onEnterOrSpace, stopPropagation } from '$utils/keyboard'; +import { onEnterOrSpace } from '$utils/keyboard'; import { mxcUrlToHttp } from '$utils/matrix'; import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; import * as css from './LobbyHero.css'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; export function LobbyHero() { const mx = useMatrixClient(); @@ -43,24 +43,13 @@ export function LobbyHero() { {(viewTopic, setViewTopic) => ( <> - }> - - setViewTopic(false), - escapeDeactivates: stopPropagation, - }} - > - setViewTopic(false)} - /> - - - + setViewTopic(false)}> + setViewTopic(false)} + /> + setViewTopic(true)} diff --git a/src/app/features/lobby/RoomItem.tsx b/src/app/features/lobby/RoomItem.tsx index 123bacce26..6f5f148264 100644 --- a/src/app/features/lobby/RoomItem.tsx +++ b/src/app/features/lobby/RoomItem.tsx @@ -6,9 +6,6 @@ import { Box, Chip, Line, - Overlay, - OverlayBackdrop, - OverlayCenter, Spinner, Text, Tooltip, @@ -17,7 +14,6 @@ import { color, toRem, } from 'folds'; -import FocusTrap from 'focus-trap-react'; import type { MatrixError, Room, IHierarchyRoom } from '$types/matrix-sdk'; import { JoinRule, KnownMembership } from '$types/matrix-sdk'; import { RoomAvatar, RoomIcon } from '$components/room-avatar'; @@ -28,7 +24,7 @@ import { KnockRoomPrompt } from '$components/knock-room-prompt'; import { LocalRoomSummaryLoader } from '$components/RoomSummaryLoader'; import { UseStateProvider } from '$components/UseStateProvider'; import { RoomTopicViewer } from '$components/room-topic-viewer'; -import { onEnterOrSpace, stopPropagation } from '$utils/keyboard'; +import { onEnterOrSpace } from '$utils/keyboard'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '$utils/room'; @@ -47,6 +43,7 @@ import { } from '$components/icons/phosphor'; import * as styleCss from './style.css'; import * as css from './RoomItem.css'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type RoomJoinButtonProps = { roomId: string; @@ -296,24 +293,13 @@ function RoomProfile({ > {topic} - }> - - setView(false), - escapeDeactivates: stopPropagation, - }} - > - setView(false)} - /> - - - + setView(false)}> + setView(false)} + /> + )} diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 2dd08bc31a..4976dc471d 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -7,9 +7,6 @@ import { Box, Avatar, Text, - Overlay, - OverlayCenter, - OverlayBackdrop, IconButton, Tooltip, TooltipProvider, @@ -117,6 +114,7 @@ import { RoomPinMenu } from './room-pin-menu'; import * as css from './RoomViewHeader.css'; import { RoomCallButton } from './RoomCallButton'; import { CustomAccountDataEvent } from '$types/matrix/accountData'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; const log = createLogger('RoomViewHeader'); @@ -664,24 +662,13 @@ export function RoomViewHeader({ callView }: Readonly<{ callView?: boolean }>) { {(viewTopic, setViewTopic) => ( <> - }> - - setViewTopic(false), - escapeDeactivates: stopPropagation, - }} - > - setViewTopic(false)} - /> - - - + setViewTopic(false)}> + setViewTopic(false)} + /> + ) )} {imageFileURL && ( - }> - - - - - - - - + + + + + )} - }> - - setAlertRemove(false), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, + setAlertRemove(false)}> + +
    - -
    - - Remove Avatar - - setAlertRemove(false)} radii="300"> - {composerIcon(X)} - -
    - - - Are you sure you want to remove profile avatar? - - - -
    - - - + + Remove Avatar + + setAlertRemove(false)} radii="300"> + {composerIcon(X)} + +
    + + + Are you sure you want to remove profile avatar? + + + +
    +
    ); } @@ -363,42 +337,31 @@ function ProfileBanner({ profile }: Readonly>) { )} - }> - - setAlertRemove(false), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, + setAlertRemove(false)}> + +
    - -
    - - Remove Banner - - setAlertRemove(false)} radii="300"> - {composerIcon(X)} - -
    - - Are you sure you want to remove profile banner? - - -
    - - - + + Remove Banner + + setAlertRemove(false)} radii="300"> + {composerIcon(X)} + +
    + + Are you sure you want to remove profile banner? + + +
    +
    ); } diff --git a/src/app/pages/client/explore/Explore.tsx b/src/app/pages/client/explore/Explore.tsx index 184ede69fd..d3195fdd02 100644 --- a/src/app/pages/client/explore/Explore.tsx +++ b/src/app/pages/client/explore/Explore.tsx @@ -1,7 +1,6 @@ import type { FormEventHandler, MouseEventHandler } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import FocusTrap from 'focus-trap-react'; import { Avatar, Box, @@ -10,9 +9,6 @@ import { Header, IconButton, Input, - Overlay, - OverlayBackdrop, - OverlayCenter, Text, color, config, @@ -45,7 +41,6 @@ import { useMatrixClient } from '$hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { useNavToActivePathMapper } from '$hooks/useNavToActivePathMapper'; import { PageNav, PageNavContent, PageNavHeader } from '$components/page'; -import { stopPropagation } from '$utils/keyboard'; import { SidebarResizer } from '$pages/client/sidebar/SidebarResizer'; import { settingsAtom } from '$state/settings'; import { useSetting } from '$state/hooks/settings'; @@ -55,6 +50,7 @@ import { useScreenSizeContext, ScreenSize } from '$hooks/useScreenSize'; import { isResizingSidebarAtom } from '$state/isResizingSidebar'; import { useSetAtom } from 'jotai'; import { UserQuickTools } from '../sidebar/UserQuickTools'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; type AddServerProps = { hideText?: boolean; @@ -110,56 +106,47 @@ export function AddServer({ hideText, onAddServer }: AddServerProps) { return ( <> - }> - - setDialog(false), - escapeDeactivates: stopPropagation, + setDialog(false)}> + +
    - -
    - - Add Server - - setDialog(false)} radii="300"> - {composerIcon(X)} - -
    - - Add server name to explore public communities. - - Server Name - - {serverError && ( - - {serverError} - - )} - {exploreState.status === AsyncStatus.Error && ( - - Failed to load public rooms. Please try again. - - )} - - - {/*
    + + Add server name to explore public communities. + + Server Name + + {serverError && ( + + {serverError} + + )} + {exploreState.status === AsyncStatus.Error && ( + + Failed to load public rooms. Please try again. + + )} + + + {/* */} - - - -
    -
    -
    -
    + + + + + {!hideText ? ( - - -
    -
    -
    + + Remove Bookmark + + + + + + + Are you sure you want to remove this bookmark? + + {renderMatrixEvent()} + + + + + ); } diff --git a/src/app/pages/client/inbox/Invites.tsx b/src/app/pages/client/inbox/Invites.tsx index ddedd279d5..f48383fbdc 100644 --- a/src/app/pages/client/inbox/Invites.tsx +++ b/src/app/pages/client/inbox/Invites.tsx @@ -6,9 +6,6 @@ import { Button, Chip, IconButton, - Overlay, - OverlayBackdrop, - OverlayCenter, Scroll, Spinner, Text, @@ -36,7 +33,6 @@ import type { Room, AccountDataEvents, } from '$types/matrix-sdk'; -import FocusTrap from 'focus-trap-react'; import { Page, PageContent, @@ -70,7 +66,7 @@ import { } from '$utils/matrix'; import { Time } from '$components/message'; import { useElementSizeObserver } from '$hooks/useElementSizeObserver'; -import { onEnterOrSpace, stopPropagation } from '$utils/keyboard'; +import { onEnterOrSpace } from '$utils/keyboard'; import { RoomTopicViewer } from '$components/room-topic-viewer'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { useRoomNavigate } from '$hooks/useRoomNavigate'; @@ -88,6 +84,7 @@ import { EventType } from '$types/matrix-sdk'; import { CustomAccountDataEvent } from '$types/matrix/accountData'; import { updateInviteList } from '$state/updateInvites'; import { useDismissedInviteList } from '$hooks/useDismissedInvites'; +import { ModalOverlay } from '$components/modal-overlay/ModalOverlay'; const COMPACT_CARD_WIDTH = 548; @@ -310,24 +307,13 @@ function InviteCard({ {invite.roomTopic}
    )} - }> - - - - - - + + + {joinState.status === AsyncStatus.Error && ( From 4241064e2ffa733d77ead837c0496fcdefbbe8be Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Sat, 25 Jul 2026 00:25:57 +0200 Subject: [PATCH 04/43] feat: open home and direct tab menus with a long press on touch --- src/app/pages/client/sidebar/DirectTab.tsx | 31 +++++++++++++++++----- src/app/pages/client/sidebar/HomeTab.tsx | 31 +++++++++++++++++----- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/app/pages/client/sidebar/DirectTab.tsx b/src/app/pages/client/sidebar/DirectTab.tsx index 7732bf46f7..0b94665398 100644 --- a/src/app/pages/client/sidebar/DirectTab.tsx +++ b/src/app/pages/client/sidebar/DirectTab.tsx @@ -1,5 +1,5 @@ -import type { MouseEventHandler } from 'react'; -import { forwardRef, useMemo, useState } from 'react'; +import type { MouseEventHandler, TouchEvent as ReactTouchEvent } from 'react'; +import { forwardRef, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import type { RectCords } from 'folds'; import { Box, Menu, MenuItem, PopOut, Text, config, toRem } from 'folds'; @@ -28,6 +28,7 @@ import { settingsAtom } from '$state/settings'; import { useSetting } from '$state/hooks/settings'; import { useDirectRooms } from '$pages/client/direct/useDirectRooms'; import { useSidebarDirectRoomIds } from './useSidebarDirectRoomIds'; +import { useMobileLongPress } from '$hooks/useMobileLongPress'; type DirectMenuProps = { requestClose: () => void; @@ -96,13 +97,25 @@ export function DirectTab() { navigate(getDirectPath()); }; + const buttonRef = useRef(null); + + const openMenuAt = (element: HTMLElement) => { + const cords = element.getBoundingClientRect(); + setMenuAnchor((currentState) => (currentState ? undefined : cords)); + }; + const handleContextMenu: MouseEventHandler = (evt) => { evt.preventDefault(); - const cords = evt.currentTarget.getBoundingClientRect(); - setMenuAnchor((currentState) => { - if (currentState) return undefined; - return cords; - }); + openMenuAt(evt.currentTarget); + }; + + const longPress = useMobileLongPress(() => { + if (buttonRef.current) openMenuAt(buttonRef.current); + }); + + const handleTouchStart = (evt: ReactTouchEvent) => { + buttonRef.current = evt.currentTarget; + longPress.onTouchStart(evt); }; return ( @@ -114,6 +127,10 @@ export function DirectTab() { outlined onClick={handleDirectClick} onContextMenu={handleContextMenu} + onTouchStart={handleTouchStart} + onTouchEnd={longPress.onTouchEnd} + onTouchMove={longPress.onTouchMove} + onTouchCancel={longPress.onTouchCancel} > (null); + + const openMenuAt = (element: HTMLElement) => { + const cords = element.getBoundingClientRect(); + setMenuAnchor((currentState) => (currentState ? undefined : cords)); + }; + const handleContextMenu: MouseEventHandler = (evt) => { evt.preventDefault(); - const cords = evt.currentTarget.getBoundingClientRect(); - setMenuAnchor((currentState) => { - if (currentState) return undefined; - return cords; - }); + openMenuAt(evt.currentTarget); + }; + + const longPress = useMobileLongPress(() => { + if (buttonRef.current) openMenuAt(buttonRef.current); + }); + + const handleTouchStart = (evt: ReactTouchEvent) => { + buttonRef.current = evt.currentTarget; + longPress.onTouchStart(evt); }; return ( @@ -105,6 +118,10 @@ export function HomeTab() { outlined onClick={handleHomeClick} onContextMenu={handleContextMenu} + onTouchStart={handleTouchStart} + onTouchEnd={longPress.onTouchEnd} + onTouchMove={longPress.onTouchMove} + onTouchCancel={longPress.onTouchCancel} > Date: Sat, 25 Jul 2026 00:38:39 +0200 Subject: [PATCH 05/43] refactor: share one FormPage across create room, create space and bug report --- src/app/components/page/FormPage.tsx | 124 +++++ .../features/bug-report/BugReportModal.tsx | 500 +++++++++--------- .../pages/client/bug-report/BugReportPage.tsx | 103 +--- .../client/create-room/CreateRoomPage.tsx | 131 +---- src/app/pages/client/create/Create.tsx | 131 +---- 5 files changed, 404 insertions(+), 585 deletions(-) create mode 100644 src/app/components/page/FormPage.tsx diff --git a/src/app/components/page/FormPage.tsx b/src/app/components/page/FormPage.tsx new file mode 100644 index 0000000000..922c631de3 --- /dev/null +++ b/src/app/components/page/FormPage.tsx @@ -0,0 +1,124 @@ +import type { ReactNode } from 'react'; +import { useEffect, useState } from 'react'; +import { useSetAtom } from 'jotai'; +import { Box, IconButton, Scroll, Text, color, config, toRem } from 'folds'; +import { SquaresFour, composerIcon, sizedIcon, X } from '$components/icons/phosphor'; +import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; +import { useSetting } from '$state/hooks/settings'; +import { settingsAtom } from '$state/settings'; +import { isResizingSidebarAtom } from '$state/isResizingSidebar'; +import { SidebarResizer } from '$pages/client/sidebar/SidebarResizer'; +import { UserQuickTools } from '$pages/client/sidebar/UserQuickTools'; +import { + Page, + PageContent, + PageContentCenter, + PageHero, + PageHeroSection, + PageNav, + PageNavHeader, +} from './Page'; + +type FormPageProps = { + title: string; + subTitle: string; + closeLabel: string; + onClose: () => void; + children: ReactNode; +}; + +export function FormPage({ title, subTitle, closeLabel, onClose, children }: FormPageProps) { + const setIsResizingSidebar = useSetAtom(isResizingSidebarAtom); + const [roomSidebarWidth, setRoomSidebarWidth] = useSetting(settingsAtom, 'roomSidebarWidth'); + const [curWidth, setCurWidth] = useState(roomSidebarWidth); + + useEffect(() => { + setCurWidth(roomSidebarWidth); + }, [roomSidebarWidth]); + + const screenSize = useScreenSizeContext(); + const isMobile = screenSize === ScreenSize.Mobile; + const hideText = curWidth <= 80 && !isMobile; + const [oldSidebar] = useSetting(settingsAtom, 'oldSidebar'); + + return ( + <> + {!isMobile && ( + + + + + {hideText ? ( + sizedIcon(SquaresFour, '200', { filled: true }) + ) : ( + + + {title} + + + )} + + + + + {!oldSidebar && } + + )} + + + {isMobile && ( + + + + + + {title} + + + + {composerIcon(X)} + + + + + + )} + + + + + + + {children} + + + + + + + + + ); +} diff --git a/src/app/features/bug-report/BugReportModal.tsx b/src/app/features/bug-report/BugReportModal.tsx index 7042f0da2f..76c21f1b7d 100644 --- a/src/app/features/bug-report/BugReportModal.tsx +++ b/src/app/features/bug-report/BugReportModal.tsx @@ -218,275 +218,248 @@ export function BugReportForm({ onDone }: { onDone: () => void }) { }; return ( - -
    - - Report an Issue + + {/* Type */} + + Type + + setType('bug')} + > + Bug Report + + setType('feature')} + > + Feature Request + - - {composerIcon(X)} - -
    - - - {/* Type */} - - Type - - setType('bug')} - > - Bug Report - - setType('feature')} - > - Feature Request - - + + + {/* Title + duplicate check */} + + Title * + setTitle((e.target as HTMLInputElement).value)} + /> + {searching && ( + + + Searching for similar issues… - - {/* Title + duplicate check */} + )} + {!searching && similarIssues.length > 0 && ( - Title * - setTitle((e.target as HTMLInputElement).value)} - /> - {searching && ( - - - Searching for similar issues… - - )} - {!searching && similarIssues.length > 0 && ( - - Similar open issues — please check before submitting: - {similarIssues.map((issue) => ( - - {'→ '} - - #{issue.number}: {issue.title} - - - ))} - - )} + Similar open issues — please check before submitting: + {similarIssues.map((issue) => ( + + {'→ '} + + #{issue.number}: {issue.title} + + + ))} - - {/* Description */} - - - {type === 'bug' ? 'Describe the bug *' : 'Describe the problem *'} - -