Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,27 @@ export default defineConfig({
testDir: './tests/e2e',
testMatch: '**/*.spec.ts',
snapshotPathTemplate: 'tests/e2e/__screenshots__/{projectName}/{testFileName}/{arg}{ext}',
// Every context shares one Matrix account and device id from the seeded
// storageState, so concurrent clients contend over sync and crypto state.
// Parallelising would need a worker-scoped account fixture; the suite runs in
// well under a minute serially, so it is not worth the plumbing.
fullyParallel: false,
workers: 1,
// Locally a failure should surface at once; CI keeps retries for flakes.
retries: process.env.CI ? 2 : 0,
reporter: [['html', { open: 'never' }], ['list']],
globalSetup: './tests/e2e/global-setup.ts',
// Tests run in 3-6s against a built app; this is headroom, not a wait budget.
timeout: 60_000,
expect: {
toHaveScreenshot: { maxDiffPixelRatio: 0.01 },
},
use: {
baseURL: 'http://localhost:8080',
baseURL: 'http://localhost:8081',
storageState: 'tests/e2e/.auth/state.json',
trace: 'on-first-retry',
// Sheets skip their entrance animation here, so a click cannot land mid-slide.
reducedMotion: 'reduce',
},
projects: [
{
Expand All @@ -29,9 +37,15 @@ export default defineConfig({
},
],
webServer: {
command: 'pnpm run build && pnpm exec vite preview --port 8080 --strictPort',
url: 'http://localhost:8080',
// A real build, not the dev server: no HMR and no dependency optimiser, both
// of which invalidate the module graph mid-run and make the suite lie.
// Port 8081, not the dev server's 8080, so a running `pnpm dev` is untouched.
command: 'pnpm run build && pnpm exec vite preview --port 8081 --strictPort',
url: 'http://localhost:8081',
// Never reuse: a leftover server would silently serve a stale build. The
// build is ~30s, which is worth paying to know what is under test.
reuseExistingServer: false,
// ~35s observed (31s build + preview start), with headroom for a cold CI box.
timeout: 180_000,
env: { NODE_OPTIONS: '--max-old-space-size=8192' },
},
Expand Down
11 changes: 11 additions & 0 deletions src/app/components/GlobalKeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { roomToUnreadAtom } from '$state/room/roomToUnread';
import { useKeyDown } from '$hooks/useKeyDown';
import {
getDirectRoomPath,
getCreateRoomPath,
getHomeRoomPath,
getHomeSearchPath,
getInboxBookmarksPath,
Expand Down Expand Up @@ -180,6 +181,15 @@ export function GlobalKeyboardShortcuts() {
[navigate, shortcutOverrides]
);

const handleCreateRoomKeyDown = useCallback(
(evt: KeyboardEvent) => {
if (!matchesShortcut('app.createRoom', evt, shortcutOverrides)) return;
evt.preventDefault();
openShallowRoute(getCreateRoomPath(currentSpace));
},
[currentSpace, openShallowRoute, shortcutOverrides]
);

/** Opens the room search palette */
const handleOpenRoomSearch = useCallback(
(evt: KeyboardEvent) => {
Expand Down Expand Up @@ -213,6 +223,7 @@ export function GlobalKeyboardShortcuts() {
useKeyDown(window, handleUnreadNavKeyDown);
useKeyDown(window, handleReplyKeyDown);
useKeyDown(window, handleBookmarkKeyDown);
useKeyDown(window, handleCreateRoomKeyDown);
useKeyDown(window, handleSearchMessageInRoom);
useKeyDown(window, handleOpenRoomSearch);

Expand Down
113 changes: 37 additions & 76 deletions src/app/components/JoinRulesSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { MouseEventHandler } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { RectCords } from 'folds';
import { config, Box, MenuItem, Text, PopOut, Menu, Button, Spinner } from 'folds';
import { useMemo } from 'react';
import { Button, Spinner, Text } from 'folds';
import { JoinRule } from '$types/matrix-sdk';
import FocusTrap from 'focus-trap-react';
import { stopPropagation } from '$utils/keyboard';
import { CaretDown, sizedIcon } from '$components/icons/phosphor';
import { getRoomIconComponent, type RoomPhosphorIcon } from '$components/icons/roomIcons';
import {
SettingMenuSelector,
type SettingMenuOption,
} from '$components/setting-menu-selector/SettingMenuSelector';

export type ExtraJoinRules = 'knock_restricted';
export type ExtendedJoinRules = JoinRule | ExtraJoinRules;
Expand Down Expand Up @@ -58,78 +58,39 @@ export function JoinRulesSwitcher<T extends ExtendedJoinRules[]>({
disabled,
changing,
}: JoinRulesSwitcherProps<T>) {
const [cords, setCords] = useState<RectCords>();

const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setCords(evt.currentTarget.getBoundingClientRect());
};

const handleChange = useCallback(
(selectedRule: ExtendedJoinRules) => {
setCords(undefined);
onChange(selectedRule);
},
[onChange]
);
const options: SettingMenuOption<ExtendedJoinRules>[] = rules.map((rule) => ({
value: rule,
label: labels[rule],
icon: sizedIcon(icons[rule], '100'),
}));

return (
<PopOut
anchor={cords}
position="Bottom"
align="End"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setCords(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
<SettingMenuSelector
value={value}
options={options}
onSelect={onChange}
disabled={disabled}
renderTrigger={({ openMenu }) => (
<Button
size="300"
variant="Secondary"
fill="Soft"
radii="300"
outlined
before={sizedIcon(icons[value] ?? icons[JoinRule.Restricted], '100')}
after={
changing ? (
<Spinner size="100" variant="Secondary" fill="Soft" />
) : (
sizedIcon(CaretDown, '100')
)
}
onClick={openMenu}
disabled={disabled}
>
<Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
{rules.map((rule) => (
<MenuItem
key={rule}
size="300"
variant="Surface"
radii="300"
aria-pressed={value === rule}
onClick={() => handleChange(rule)}
before={sizedIcon(icons[rule], '100')}
disabled={disabled}
>
<Box grow="Yes">
<Text size="T300">{labels[rule]}</Text>
</Box>
</MenuItem>
))}
</Box>
</Menu>
</FocusTrap>
}
>
<Button
size="300"
variant="Secondary"
fill="Soft"
radii="300"
outlined
before={sizedIcon(icons[value] ?? icons[JoinRule.Restricted], '100')}
after={
changing ? (
<Spinner size="100" variant="Secondary" fill="Soft" />
) : (
sizedIcon(CaretDown, '100')
)
}
onClick={handleOpenMenu}
disabled={disabled}
>
<Text size="B300">{labels[value] ?? 'Unsupported'}</Text>
</Button>
</PopOut>
<Text size="B300">{labels[value] ?? 'Unsupported'}</Text>
</Button>
)}
/>
);
}
14 changes: 14 additions & 0 deletions src/app/components/LogoutDialogOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ModalOverlay } from '$components/modal-overlay/ModalOverlay';
import { LogoutDialog } from '$components/LogoutDialog';

type LogoutDialogOverlayProps = {
requestClose: () => void;
};

export function LogoutDialogOverlay({ requestClose }: LogoutDialogOverlayProps) {
return (
<ModalOverlay requestClose={requestClose}>
<LogoutDialog handleClose={requestClose} />
</ModalOverlay>
);
}
14 changes: 8 additions & 6 deletions src/app/components/MemberSortMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import FocusTrap from 'focus-trap-react';
import { config, Menu, MenuItem, Text } from 'folds';
import { stopPropagation } from '$utils/keyboard';
import { useMemberSortMenu } from '$hooks/useMemberSort';

type MemberSortMenuProps = {
type MemberMenuItem = {
name: string;
};

type MemberMenuListProps = {
items: MemberMenuItem[];
requestClose: () => void;
selected: number;
onSelect: (index: number) => void;
};
export function MemberSortMenu({ selected, onSelect, requestClose }: MemberSortMenuProps) {
const memberSortMenu = useMemberSortMenu();

export function MemberMenuList({ items, selected, onSelect, requestClose }: MemberMenuListProps) {
return (
<FocusTrap
focusTrapOptions={{
Expand All @@ -23,7 +25,7 @@ export function MemberSortMenu({ selected, onSelect, requestClose }: MemberSortM
}}
>
<Menu style={{ padding: config.space.S100 }}>
{memberSortMenu.map((menuItem, index) => (
{items.map((menuItem, index) => (
<MenuItem
key={menuItem.name}
variant="Surface"
Expand Down
48 changes: 0 additions & 48 deletions src/app/components/MembershipFilterMenu.tsx

This file was deleted.

57 changes: 9 additions & 48 deletions src/app/components/Modal500.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,22 @@
import type { ReactNode } from 'react';
import { useRef } from 'react';
import FocusTrap from 'focus-trap-react';
import { Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize';
import { stopPropagation } from '$utils/keyboard';
import { useDismissOnBack } from '$utils/androidBack';
import { Modal } from 'folds';
import { ModalOverlay } from '$components/modal-overlay/ModalOverlay';

type Modal500Props = {
requestClose: () => void;
children: ReactNode;
};

/** A size-500 modal on desktop that fills the viewport on a phone. */
export function Modal500({ requestClose, children }: Modal500Props) {
const modalRef = useRef<HTMLDivElement | null>(null);
const screenSize = useScreenSizeContext();

// Android back closes the overlay instead of navigating away.
useDismissOnBack(requestClose);

if (screenSize === ScreenSize.Mobile) {
return (
<Overlay open>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
escapeDeactivates: stopPropagation,
onDeactivate: requestClose,
}}
>
<div
ref={modalRef}
tabIndex={-1}
style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column' }}
>
{children}
</div>
</FocusTrap>
</Overlay>
);
}

return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
fallbackFocus: () => modalRef.current ?? document.body,
clickOutsideDeactivates: true,
onDeactivate: requestClose,
escapeDeactivates: stopPropagation,
}}
>
<Modal ref={modalRef} tabIndex={-1} size="500" variant="Background">
{children}
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
<ModalOverlay requestClose={requestClose} mobile="fullscreen" contentRef={modalRef}>
<Modal ref={modalRef} tabIndex={-1} size="500" variant="Background">
{children}
</Modal>
</ModalOverlay>
);
}
Loading
Loading