import { useCallback, useRef } from 'react'; import { NavigateOptions, useLocation, useNavigate } from 'react-router-dom'; import { useAtomValue } from 'jotai'; import { getCanonicalAliasOrRoomId } from '../utils/matrix'; import { getChannelsRoomPath, getDirectRoomPath, getSpacePath } from '../pages/pathUtils'; import { useMatrixClient } from './useMatrixClient'; import { getOrphanParents, guessPerfectParent, isSpace } from '../utils/room'; import { roomToParentsAtom } from '../state/room/roomToParents'; import { useSelectedSpace } from './router/useSelectedSpace'; import { settingsAtom } from '../state/settings'; import { useSetting } from '../state/hooks/settings'; export const useRoomNavigate = () => { const navigate = useNavigate(); const location = useLocation(); const mx = useMatrixClient(); const roomToParents = useAtomValue(roomToParentsAtom); const spaceSelectedId = useSelectedSpace(); const [developerTools] = useSetting(settingsAtom, 'developerTools'); // navigate(samePath) creates a fresh history entry in react-router-v6 even // though the screen does not change — repeated taps on the same chat from // search/inbox/notifications would inflate the back-stack tracked by // useAndroidBackButton. Force replace when the target matches the current // pathname so callers do not need to pass it everywhere. // // Pathname tracking has two sources: // 1. useLocation → pathnameRef on every render, for consistency with the // router after it commits a navigation we did not initiate. // 2. Optimistic write inside safeNavigate, so two calls in the same tick // (before React commits the first) collapse correctly: the second sees // the target the first just navigated to and forces replace. // Without (2), two simultaneous navigateRoom(samePath) calls — e.g. two // notifications dispatched back-to-back, or a double-clicked list item — // both compare against the pre-navigation pathname and both PUSH. Without // (1), an external navigate() between our calls would leave pathnameRef // ahead of the actual route. // // The callback identity stays stable (deps = [navigate] only) so consumers // that wrap navigateRoom in their own useCallback do not capture a stale // closure on every route change. const pathnameRef = useRef(location.pathname); pathnameRef.current = location.pathname; const safeNavigate = useCallback( (target: string, opts?: NavigateOptions) => { const finalOpts = pathnameRef.current === target ? { ...opts, replace: true } : opts; navigate(target, finalOpts); pathnameRef.current = target; }, [navigate] ); const navigateSpace = useCallback( (roomId: string) => { const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId); safeNavigate(getSpacePath(roomIdOrAlias)); }, [mx, safeNavigate] ); const navigateRoom = useCallback( (roomId: string, eventId?: string, opts?: NavigateOptions) => { const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId); // Developer-tools edge case: clicking the rail-selected space row // opens the space's own timeline as a pseudo-room. Route through // /channels/// so the dev view shares the channels // SpaceRouteRoomProvider — its `developerTools && isSpaceRoom && // room.roomId === space.roomId` branch already permits this. if (developerTools && spaceSelectedId === roomId) { safeNavigate(getChannelsRoomPath(roomIdOrAlias, roomIdOrAlias, eventId), opts); return; } const orphanParents = getOrphanParents(roomToParents, roomId); if (orphanParents.length > 0) { let parentSpace: string; if (spaceSelectedId && orphanParents.includes(spaceSelectedId)) { parentSpace = spaceSelectedId; } else { parentSpace = guessPerfectParent(mx, roomId, orphanParents) ?? orphanParents[0]; } const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace); safeNavigate(getChannelsRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts); return; } // After P3c the Direct tab is universal — every non-space leaf room // routes through /direct/. Space roots stay on their own /{spaceId}/ // route handled by navigateSpace. const targetRoom = mx.getRoom(roomId); if (targetRoom && isSpace(targetRoom)) { safeNavigate(getSpacePath(roomIdOrAlias), opts); return; } safeNavigate(getDirectRoomPath(roomIdOrAlias, eventId), opts); }, [mx, safeNavigate, spaceSelectedId, roomToParents, developerTools] ); return { navigateSpace, navigateRoom, }; };