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
5 changes: 5 additions & 0 deletions .changeset/fix-android-room-context-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Activate room context menu items on pointerup on Android to work around WebView click synthesis being suppressed after the long-press gesture that opens the mobile menu.
23 changes: 23 additions & 0 deletions src/app/components/MobileMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ComponentProps, MouseEventHandler } from 'react';
import { MenuItem } from 'folds';
import { useMobileTapActivation } from '$hooks/useMobileTapActivation';

type MenuItemProps = ComponentProps<typeof MenuItem>;

type MobileMenuItemProps = Omit<MenuItemProps, 'onClick'> & {
isMobile: boolean;
onClick?: MouseEventHandler<HTMLButtonElement>;
};

/**
* Wraps folds' `MenuItem` with pointer-based tap activation for Android
* WebView, where click synthesis is suppressed after the long-press gesture
* that opens the mobile swipe-down modal. On mobile the `onClick` handler
* fires on `pointerup` instead of relying on the missing click event.
*/
export function MobileMenuItem({ isMobile, onClick, ...props }: MobileMenuItemProps) {
const activation = useMobileTapActivation<HTMLButtonElement>(isMobile, (evt) => {
onClick?.(evt);
});
return <MenuItem onClick={onClick} {...activation} {...props} />;
}
53 changes: 38 additions & 15 deletions src/app/features/room-nav/RoomNavItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
IconButton,
Text,
Menu,
MenuItem,
config,
PopOut,
toRem,
Expand Down Expand Up @@ -62,6 +61,7 @@ import {
} from '$components/icons/phosphor';
import { Copy as CopyIcon } from '@phosphor-icons/react';
import { MobileSwipeDownModal } from '$components/MobileSwipeDownModal';
import { MobileMenuItem } from '$components/MobileMenuItem';
import { type DragOptsProps } from '$components/message/modals/Options';
import * as messageCss from '$features/room/message/styles.css';
import {
Expand Down Expand Up @@ -125,6 +125,7 @@ type RoomNavItemMenuProps = {
const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
({ room, requestClose, notificationMode, dragOpts }, ref) => {
const mx = useMatrixClient();
const isMobile = useScreenSizeContext() === ScreenSize.Mobile;
const [hideReads] = useSetting(settingsAtom, 'hideReads');
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room);
Expand Down Expand Up @@ -184,7 +185,8 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
/>
)}
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem
<MobileMenuItem
isMobile={isMobile}
onClick={handleMarkAsRead}
size="300"
after={menuIcon(Checks)}
Expand All @@ -194,10 +196,11 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Mark as Read
</Text>
</MenuItem>
</MobileMenuItem>
<RoomNotificationModeSwitcher roomId={room.roomId} value={notificationMode}>
{(handleOpen, opened, changing) => (
<MenuItem
<MobileMenuItem
isMobile={isMobile}
size="300"
after={
changing ? (
Expand All @@ -213,13 +216,14 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Notifications
</Text>
</MenuItem>
</MobileMenuItem>
)}
</RoomNotificationModeSwitcher>
</Box>
<Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem
<MobileMenuItem
isMobile={isMobile}
onClick={handleInvite}
variant="Primary"
fill="None"
Expand All @@ -232,29 +236,48 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Invite
</Text>
</MenuItem>
<MenuItem onClick={handleCopyLink} size="300" after={menuIcon(Link)} radii="300">
</MobileMenuItem>
<MobileMenuItem
isMobile={isMobile}
onClick={handleCopyLink}
size="300"
after={menuIcon(Link)}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Copy Link
</Text>
</MenuItem>
<MenuItem onClick={handleCopyName} size="300" after={menuIcon(CopyIcon)} radii="300">
</MobileMenuItem>
<MobileMenuItem
isMobile={isMobile}
onClick={handleCopyName}
size="300"
after={menuIcon(CopyIcon)}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Copy Room Name
</Text>
</MenuItem>
<MenuItem onClick={handleRoomSettings} size="300" after={menuIcon(GearSix)} radii="300">
</MobileMenuItem>
<MobileMenuItem
isMobile={isMobile}
onClick={handleRoomSettings}
size="300"
after={menuIcon(GearSix)}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Room Settings
</Text>
</MenuItem>
</MobileMenuItem>
</Box>
<Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<UseStateProvider initial={false}>
{(promptLeave, setPromptLeave) => (
<>
<MenuItem
<MobileMenuItem
isMobile={isMobile}
onClick={() => setPromptLeave(true)}
variant="Critical"
fill="None"
Expand All @@ -266,7 +289,7 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Leave Room
</Text>
</MenuItem>
</MobileMenuItem>
{promptLeave && (
<LeaveRoomPrompt
roomId={room.roomId}
Expand Down
6 changes: 3 additions & 3 deletions src/app/hooks/useMobileTapActivation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PointerEventHandler } from 'react';
import type { PointerEventHandler, PointerEvent as ReactPointerEvent } from 'react';
import { useRef } from 'react';

const TAP_MOVEMENT_THRESHOLD = 10;
Expand All @@ -10,7 +10,7 @@ const MAX_TAP_DURATION = 500;
// or defer it. That reintroduces the double-tap.
export function useMobileTapActivation<T extends HTMLElement>(
enabled: boolean,
onActivate: () => void
onActivate: (evt: ReactPointerEvent<T>) => void
): {
onPointerDown: PointerEventHandler<T>;
onPointerMove: PointerEventHandler<T>;
Expand Down Expand Up @@ -70,7 +70,7 @@ export function useMobileTapActivation<T extends HTMLElement>(
}

evt.preventDefault();
onActivateRef.current();
onActivateRef.current(evt);
};
const onPointerCancel: PointerEventHandler<T> = (evt) => {
if (evt.pointerId !== pointerDownRef.current?.pointerId) return;
Expand Down
Loading