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
38 changes: 13 additions & 25 deletions src/app/features/room-nav/RoomNavItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MouseEventHandler, MouseEvent, PointerEventHandler } from 'react';
import { forwardRef, startTransition, useState, useEffect, useRef } from 'react';
import type { MouseEventHandler, MouseEvent } from 'react';
import { forwardRef, startTransition, useState, useEffect } from 'react';
import type { Room } from '$types/matrix-sdk';
import { RoomEvent as RoomEventEnum } from '$types/matrix-sdk';
import type { RectCords } from 'folds';
Expand Down Expand Up @@ -78,6 +78,7 @@ import { useRoomName, useRoomTopic } from '$hooks/useRoomMeta';
import { nicknamesAtom } from '$state/nicknames';
import { useRoomNavigate } from '$hooks/useRoomNavigate';
import { warmupRoomDecryption } from '$utils/decryptScheduler';
import { useMobileTapActivation } from '$hooks/useMobileTapActivation';

// Call Hooks & Plugins
import { useCallMembers, useCallSession } from '$hooks/useCall';
Expand Down Expand Up @@ -418,27 +419,9 @@ export function RoomNavItem({
}
};

// Android WebView suppresses click synthesis after a drag gesture, so the
// first tap on a room row after swiping produces no click event. Navigate
// directly on pointerup when the touch had minimal movement (i.e. a tap).
const navPointerDownRef = useRef<{ x: number; y: number } | null>(null);
const handleNavPointerDown: PointerEventHandler<HTMLElement> = (evt) => {
warmupRoomDecryption(mx, room.roomId);
if (isMobile && evt.pointerType === 'touch') {
navPointerDownRef.current = { x: evt.clientX, y: evt.clientY };
}
};
const handleNavPointerUp: PointerEventHandler<HTMLElement> = (evt) => {
if (!isMobile || evt.pointerType !== 'touch' || !navPointerDownRef.current) return;
const down = navPointerDownRef.current;
navPointerDownRef.current = null;
const dx = Math.abs(evt.clientX - down.x);
const dy = Math.abs(evt.clientY - down.y);
if (dx > 10 || dy > 10) return; // was a drag, not a tap
if (room.isCallRoom()) return; // call rooms use onClick
evt.preventDefault();
startTransition(() => navigate(linkPath));
};
const mobileTapActivation = useMobileTapActivation(isMobile && !room.isCallRoom(), () => {
navigate(linkPath);
});

const handleChatButtonClick = (evt: MouseEvent<HTMLButtonElement>) => {
evt.stopPropagation();
Expand Down Expand Up @@ -506,8 +489,13 @@ export function RoomNavItem({
{(triggerRef) => (
<NavButton
onClick={handleNavItemClick}
onPointerDown={handleNavPointerDown}
onPointerUp={handleNavPointerUp}
onPointerDown={(evt) => {
warmupRoomDecryption(mx, room.roomId);
mobileTapActivation.onPointerDown(evt);
}}
onPointerMove={mobileTapActivation.onPointerMove}
onPointerUp={mobileTapActivation.onPointerUp}
onPointerCancel={mobileTapActivation.onPointerCancel}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
onTouchMove={onTouchMove}
Expand Down
81 changes: 81 additions & 0 deletions src/app/hooks/useMobileTapActivation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { PointerEventHandler } from 'react';
import { 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.
export function useMobileTapActivation<T extends HTMLElement>(
enabled: boolean,
onActivate: () => void
): {
onPointerDown: PointerEventHandler<T>;
onPointerMove: PointerEventHandler<T>;
onPointerUp: PointerEventHandler<T>;
onPointerCancel: PointerEventHandler<T>;
} {
const onActivateRef = useRef(onActivate);
const pointerDownRef = useRef<{
pointerId: number;
x: number;
y: number;
timestamp: number;
eligible: boolean;
} | null>(null);
onActivateRef.current = onActivate;

const onPointerDown: PointerEventHandler<T> = (evt) => {
if (!enabled || evt.pointerType !== 'touch' || !evt.isPrimary || evt.button !== 0) {
pointerDownRef.current = null;
return;
}

pointerDownRef.current = {
pointerId: evt.pointerId,
x: evt.clientX,
y: evt.clientY,
timestamp: evt.timeStamp,
eligible: true,
};
};
const onPointerMove: PointerEventHandler<T> = (evt) => {
const pointerDown = pointerDownRef.current;
if (!pointerDown || evt.pointerId !== pointerDown.pointerId) return;

if (
Math.abs(evt.clientX - pointerDown.x) > TAP_MOVEMENT_THRESHOLD ||
Math.abs(evt.clientY - pointerDown.y) > TAP_MOVEMENT_THRESHOLD
) {
pointerDown.eligible = false;
}
};
const onPointerUp: PointerEventHandler<T> = (evt) => {
const pointerDown = pointerDownRef.current;
if (!pointerDown || evt.pointerId !== pointerDown.pointerId) return;

pointerDownRef.current = null;
if (
!enabled ||
evt.pointerType !== 'touch' ||
!evt.isPrimary ||
!pointerDown.eligible ||
Math.abs(evt.clientX - pointerDown.x) > TAP_MOVEMENT_THRESHOLD ||
Math.abs(evt.clientY - pointerDown.y) > TAP_MOVEMENT_THRESHOLD ||
evt.timeStamp - pointerDown.timestamp >= MAX_TAP_DURATION
) {
return;
}

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

return { onPointerDown, onPointerMove, onPointerUp, onPointerCancel };
}
3 changes: 3 additions & 0 deletions src/app/pages/client/sidebar/InboxTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Text, Box, color } from 'folds';
import { searchModalAtom } from '$state/searchModal';
import { EnvelopeSimple, getPhosphorIconSize, Tray } from '$components/icons/phosphor';
import { BookmarkIcon, ChatCircleDotsIcon } from '@phosphor-icons/react';
import { useMobileTapActivation } from '$hooks/useMobileTapActivation';

export function InboxTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?: boolean }) {
const screenSize = useScreenSizeContext();
Expand Down Expand Up @@ -54,6 +55,7 @@ export function InboxTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?
const path = inviteCount > 0 ? getInboxInvitesPath() : getInboxNotificationsPath();
navigate(path);
};
const mobileTapActivation = useMobileTapActivation(isMobile ?? false, handleInboxClick);

return (
<SidebarItem active={opened && !isMobile} isBottom={isBottom}>
Expand All @@ -65,6 +67,7 @@ export function InboxTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?
ref={triggerRef}
outlined={!isMobile}
onClick={handleInboxClick}
{...mobileTapActivation}
size={'400'}
>
{(notificationsSelected && (
Expand Down
3 changes: 3 additions & 0 deletions src/app/pages/client/sidebar/MessageTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { roomToUnreadAtom } from '$state/room/roomToUnread';
import { useSetting } from '$state/hooks/settings';
import { settingsAtom } from '$state/settings';
import { resolveUnreadBadgeMode } from '$components/unread-badge';
import { useMobileTapActivation } from '$hooks/useMobileTapActivation';

export function MessageTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?: boolean }) {
const rooms = useAtomValue(allRoomsAtom);
Expand All @@ -48,6 +49,7 @@ export function MessageTab({ isBottom, isMobile }: { isBottom?: boolean; isMobil

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

const [showUnreadCounts] = useSetting(settingsAtom, 'showUnreadCounts');
const [badgeCountDMsOnly] = useSetting(settingsAtom, 'badgeCountDMsOnly');
Expand All @@ -72,6 +74,7 @@ export function MessageTab({ isBottom, isMobile }: { isBottom?: boolean; isMobil
ref={triggerRef}
outlined={!isMobile}
onClick={onBack}
{...mobileTapActivation}
size={'400'}
>
<ChatTextIcon
Expand Down
3 changes: 3 additions & 0 deletions src/app/pages/client/sidebar/NavigateTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Text, Box, color } from 'folds';
import { useNavigate } from 'react-router-dom';
import { getNavigatePath } from '$pages/pathUtils';
import { useNavigateSelected } from '$hooks/router/useNavigateSelected';
import { useMobileTapActivation } from '$hooks/useMobileTapActivation';

export function NavigateTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?: boolean }) {
const [opened, setOpen] = useAtom(searchModalAtom);
Expand All @@ -17,6 +18,7 @@ export function NavigateTab({ isBottom, isMobile }: { isBottom?: boolean; isMobi
if (isMobile) navigate(getNavigatePath());
else setOpen(true);
};
const mobileTapActivation = useMobileTapActivation(isMobile ?? false, open);

return (
<SidebarItem active={opened && !isMobile} isBottom={isBottom}>
Expand All @@ -28,6 +30,7 @@ export function NavigateTab({ isBottom, isMobile }: { isBottom?: boolean; isMobi
ref={triggerRef}
outlined={!isMobile}
onClick={open}
{...mobileTapActivation}
size={'400'}
>
<ListMagnifyingGlassIcon
Expand Down
27 changes: 13 additions & 14 deletions src/app/pages/client/sidebar/SpaceTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import { useRoomPermissions } from '$hooks/useRoomPermissions';
import { InviteUserPrompt } from '$components/invite-user-prompt';
import { CustomAccountDataEvent } from '$types/matrix/accountData';
import { lastVisitedSpaceIdAtom } from '$state/room/lastSpace';
import { useMobileTapActivation } from '$hooks/useMobileTapActivation';

type SpaceMenuProps = {
room: Room;
Expand Down Expand Up @@ -537,7 +538,7 @@ function SpaceAvatar({ space, renderFallback }: Readonly<SpaceAvatarProps>) {
type SpaceTabProps = {
space: Room;
selected: boolean;
onClick: MouseEventHandler<HTMLButtonElement>;
onSelect: (spaceId: string) => void;
folder?: ISidebarFolder;
onDragging: (dragItem?: SidebarDraggable) => void;
disabled?: boolean;
Expand All @@ -546,14 +547,15 @@ type SpaceTabProps = {
function SpaceTab({
space,
selected,
onClick,
onSelect,
folder,
onDragging,
disabled,
onUnpin,
}: Readonly<SpaceTabProps>) {
const isMobile = useScreenSizeContext() === ScreenSize.Mobile;
const targetRef = useRef<HTMLDivElement>(null);
const mobileTapActivation = useMobileTapActivation(isMobile, () => onSelect(space.roomId));

const spaceDraggable: SidebarDraggable = useMemo(
() =>
Expand Down Expand Up @@ -600,8 +602,9 @@ function SpaceTab({
data-id={space.roomId}
ref={triggerRef}
size={folder ? '300' : '400'}
onClick={onClick}
onClick={() => onSelect(space.roomId)}
onContextMenu={handleContextMenu}
{...mobileTapActivation}
>
<SpaceAvatar
space={space}
Expand Down Expand Up @@ -958,25 +961,21 @@ export function SpaceTabs({ scrollRef }: Readonly<SpaceTabsProps>) {

const selectedSpaceId = useSelectedSpace();

const handleSpaceClick: MouseEventHandler<HTMLButtonElement> = (evt) => {
const target = evt.currentTarget;
const targetSpaceId = target.getAttribute('data-id');
if (!targetSpaceId) return;

setLastSpaceId(targetSpaceId);
const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, targetSpaceId));
const handleSpaceClick = (spaceId: string) => {
setLastSpaceId(spaceId);
const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, spaceId));
if (screenSize === ScreenSize.Mobile) {
navigate(spacePath);
return;
}

const activePath = navToActivePath.get(targetSpaceId);
const activePath = navToActivePath.get(spaceId);
if (activePath?.pathname.startsWith(spacePath)) {
navigate(joinPathComponent(activePath));
return;
}

navigate(getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, targetSpaceId)));
navigate(getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, spaceId)));
};

const handleFolderToggle: MouseEventHandler<HTMLButtonElement> = (evt) => {
Expand Down Expand Up @@ -1061,7 +1060,7 @@ export function SpaceTabs({ scrollRef }: Readonly<SpaceTabsProps>) {
key={space.roomId}
space={space}
selected={space.roomId === selectedSpaceId}
onClick={handleSpaceClick}
onSelect={handleSpaceClick}
folder={item}
onDragging={setDraggingItem}
disabled={
Expand Down Expand Up @@ -1100,7 +1099,7 @@ export function SpaceTabs({ scrollRef }: Readonly<SpaceTabsProps>) {
key={space.roomId}
space={space}
selected={space.roomId === selectedSpaceId}
onClick={handleSpaceClick}
onSelect={handleSpaceClick}
onDragging={setDraggingItem}
disabled={typeof draggingItem === 'string' ? draggingItem === space.roomId : false}
onUnpin={orphanSpaces.includes(space.roomId) ? undefined : handleUnpin}
Expand Down
7 changes: 6 additions & 1 deletion src/app/pages/client/sidebar/UserMenuTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
} from '@phosphor-icons/react';
import * as css from './UserMenuTab.css';
import { getMxIdServer } from '$utils/mxIdHelper';
import { useMobileTapActivation } from '$hooks/useMobileTapActivation';

const log = createLogger('AccountSwitcherTab');

Expand Down Expand Up @@ -681,6 +682,9 @@ export function UserMenuTab({ isBottom, isMobile }: { isBottom?: boolean; isMobi
};

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

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

Expand All @@ -690,6 +694,7 @@ export function UserMenuTab({ isBottom, isMobile }: { isBottom?: boolean; isMobi
direction="Column"
alignItems="Center"
onClick={handleToggle}
{...mobileTapActivation}
style={
isMobile
? {
Expand All @@ -702,7 +707,7 @@ export function UserMenuTab({ isBottom, isMobile }: { isBottom?: boolean; isMobi
}
>
<AvatarPresence badge={<PresenceBadge presence={currentPresence} size="200" />}>
<SidebarAvatar size={isMobile ? '300' : '400'} as="button" onClick={handleToggle}>
<SidebarAvatar size={isMobile ? '300' : '400'} as="button">
<UserAvatar
userId={userId}
src={avatarUrl}
Expand Down
Loading