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-mobile-back-double-nav.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Stop opening rooms (and other mobile nav surfaces) twice on a single tap, which made backing out take two back presses.
2 changes: 1 addition & 1 deletion src/app/components/MobileMenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ export function MobileMenuItem({ isMobile, onClick, ...props }: MobileMenuItemPr
const activation = useMobileTapActivation<HTMLButtonElement>(isMobile, (evt) => {
onClick?.(evt);
});
return <MenuItem onClick={onClick} {...activation} {...props} />;
return <MenuItem {...activation} {...props} />;
}
20 changes: 12 additions & 8 deletions src/app/features/room-nav/RoomNavItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -448,13 +448,17 @@ export function RoomNavItem({
}
};

const mobileTapActivation = useMobileTapActivation(isMobile && !room.isCallRoom(), () => {
if (openMobileDrawerContent) {
openMobileDrawerContent(linkPath);
} else {
navigate(linkPath);
}
});
const mobileTapActivation = useMobileTapActivation(
isMobile && !room.isCallRoom(),
() => {
if (openMobileDrawerContent) {
openMobileDrawerContent(linkPath);
} else {
navigate(linkPath);
}
},
handleNavItemClick
);

const handleChatButtonClick = (evt: MouseEvent<HTMLButtonElement>) => {
evt.stopPropagation();
Expand Down Expand Up @@ -521,7 +525,7 @@ export function RoomNavItem({
>
{(triggerRef) => (
<NavButton
onClick={handleNavItemClick}
onClick={mobileTapActivation.onClick}
onPointerDown={(evt) => {
warmupRoomDecryption(mx, room.roomId);
mobileTapActivation.onPointerDown(evt);
Expand Down
44 changes: 36 additions & 8 deletions src/app/hooks/useMobileTapActivation.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,49 @@
import type { PointerEventHandler, PointerEvent as ReactPointerEvent } from 'react';
import { useRef } from 'react';
import type { MouseEvent as ReactMouseEvent, MouseEventHandler, PointerEventHandler } from 'react';
import { useCallback, useRef } from 'react';

const TAP_MOVEMENT_THRESHOLD = 10;
const MAX_TAP_DURATION = 500;

// Android WebView suppresses click synthesis after a drag, so the first tap
// on a nav control after swiping the drawer produces no click event. Activate
// directly on pointerup instead. Do not wrap the callback in startTransition
// or defer it. That reintroduces the double-tap.
// Android WebView suppresses click synthesis after a drag gesture, so the first
// tap on a nav control after swiping the drawer produces no click event. Activate
// directly on pointerup instead, and swallow the synthetic click that follows a
// touch tap so the action fires exactly once. (preventDefault on pointerup does
// NOT suppress click — click is an activation event, not a compatibility mouse
// event — so the dedup is handled by the returned `onClick` wrapper below.)
//
// `onActivate` runs on a qualifying touch pointerup. The returned `onClick`
// wraps the caller's click handler: if a touch tap already activated, the click
// is swallowed; otherwise the click handler runs (desktop, keyboard, AT, and
// the post-swipe case where no click arrives). Do not wrap the callbacks in
// startTransition or defer them — that reintroduces the double-tap.
export function useMobileTapActivation<T extends HTMLElement>(
enabled: boolean,
onActivate: (evt: ReactPointerEvent<T>) => void
onActivate: (evt: ReactMouseEvent<T>) => void,
onClick?: MouseEventHandler<T>
): {
onPointerDown: PointerEventHandler<T>;
onPointerMove: PointerEventHandler<T>;
onPointerUp: PointerEventHandler<T>;
onPointerCancel: PointerEventHandler<T>;
onClick: MouseEventHandler<T>;
} {
const onActivateRef = useRef(onActivate);
const onClickRef = useRef(onClick);
const pointerDownRef = useRef<{
pointerId: number;
x: number;
y: number;
timestamp: number;
eligible: boolean;
} | null>(null);
// True between a qualifying touch pointerup and the click it synthesises.
// Reset on every pointerdown so a stale flag can never outlive one tap.
const activatedRef = useRef(false);
onActivateRef.current = onActivate;
onClickRef.current = onClick;

const onPointerDown: PointerEventHandler<T> = (evt) => {
activatedRef.current = false;
if (!enabled || evt.pointerType !== 'touch' || !evt.isPrimary || evt.button !== 0) {
pointerDownRef.current = null;
return;
Expand Down Expand Up @@ -70,12 +86,24 @@ export function useMobileTapActivation<T extends HTMLElement>(
}

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

return { onPointerDown, onPointerMove, onPointerUp, onPointerCancel };
// Swallow the synthetic click that follows a touch pointerup; otherwise run
// the caller's click handler (or onActivate when none was provided).
const handleClick: MouseEventHandler<T> = useCallback((evt) => {
if (activatedRef.current) {
activatedRef.current = false;
return;
}
if (onClickRef.current) onClickRef.current(evt);
else onActivateRef.current(evt);
}, []);

return { onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onClick: handleClick };
}
7 changes: 5 additions & 2 deletions src/app/pages/client/sidebar/InboxTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ export function InboxTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?
const path = inviteCount > 0 ? getInboxInvitesPath() : getInboxNotificationsPath();
navigate(path);
};
const mobileTapActivation = useMobileTapActivation(isMobile ?? false, handleInboxClick);
const mobileTapActivation = useMobileTapActivation(
isMobile ?? false,
handleInboxClick,
handleInboxClick
);

return (
<SidebarItem active={opened && !isMobile} isBottom={isBottom}>
Expand All @@ -66,7 +70,6 @@ export function InboxTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?
as="button"
ref={triggerRef}
outlined={!isMobile}
onClick={handleInboxClick}
{...mobileTapActivation}
size={'400'}
>
Expand Down
3 changes: 1 addition & 2 deletions src/app/pages/client/sidebar/MessageTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function MessageTab({ isBottom, isMobile }: { isBottom?: boolean; isMobil

navigate(getSpacePath(lastSpaceId));
};
const mobileTapActivation = useMobileTapActivation(isMobile ?? false, onBack);
const mobileTapActivation = useMobileTapActivation(isMobile ?? false, onBack, onBack);

const [showUnreadCounts] = useSetting(settingsAtom, 'showUnreadCounts');
const [badgeCountDMsOnly] = useSetting(settingsAtom, 'badgeCountDMsOnly');
Expand All @@ -73,7 +73,6 @@ export function MessageTab({ isBottom, isMobile }: { isBottom?: boolean; isMobil
as="button"
ref={triggerRef}
outlined={!isMobile}
onClick={onBack}
{...mobileTapActivation}
size={'400'}
>
Expand Down
3 changes: 1 addition & 2 deletions src/app/pages/client/sidebar/NavigateTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function NavigateTab({ isBottom, isMobile }: { isBottom?: boolean; isMobi
if (isMobile) navigate(getNavigatePath());
else setOpen(true);
};
const mobileTapActivation = useMobileTapActivation(isMobile ?? false, open);
const mobileTapActivation = useMobileTapActivation(isMobile ?? false, open, open);

return (
<SidebarItem active={opened && !isMobile} isBottom={isBottom}>
Expand All @@ -29,7 +29,6 @@ export function NavigateTab({ isBottom, isMobile }: { isBottom?: boolean; isMobi
as="button"
ref={triggerRef}
outlined={!isMobile}
onClick={open}
{...mobileTapActivation}
size={'400'}
>
Expand Down
7 changes: 5 additions & 2 deletions src/app/pages/client/sidebar/SpaceTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,11 @@ function SpaceTab({
}: Readonly<SpaceTabProps>) {
const isMobile = useScreenSizeContext() === ScreenSize.Mobile;
const targetRef = useRef<HTMLDivElement>(null);
const mobileTapActivation = useMobileTapActivation(isMobile, () => onSelect(space.roomId));
const mobileTapActivation = useMobileTapActivation(
isMobile,
() => onSelect(space.roomId),
() => onSelect(space.roomId)
);

const spaceDraggable: SidebarDraggable = useMemo(
() =>
Expand Down Expand Up @@ -602,7 +606,6 @@ function SpaceTab({
data-id={space.roomId}
ref={triggerRef}
size={folder ? '300' : '400'}
onClick={() => onSelect(space.roomId)}
onContextMenu={handleContextMenu}
{...mobileTapActivation}
>
Expand Down
11 changes: 7 additions & 4 deletions src/app/pages/client/sidebar/UserMenuTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -682,9 +682,13 @@ export function UserMenuTab({ isBottom, isMobile }: { isBottom?: boolean; isMobi
};

const handleCloseMenu = () => setMenuAnchor(undefined);
const mobileTapActivation = useMobileTapActivation(isMobile ?? false, () => {
navigate(getProfilePath());
});
const mobileTapActivation = useMobileTapActivation(
isMobile ?? false,
() => {
navigate(getProfilePath());
},
handleToggle
);

const isActive = (!!menuAnchor || profileSelected) && !isMobile;

Expand All @@ -693,7 +697,6 @@ export function UserMenuTab({ isBottom, isMobile }: { isBottom?: boolean; isMobi
<Box
direction="Column"
alignItems="Center"
onClick={handleToggle}
{...mobileTapActivation}
style={
isMobile
Expand Down
Loading