114 lines
4.7 KiB
TypeScript
114 lines
4.7 KiB
TypeScript
import { useCallback, useRef } from 'react';
|
|
import { NavigateOptions, useLocation, useNavigate } from 'react-router-dom';
|
|
import { useAtomValue } from 'jotai';
|
|
import { getCanonicalAliasOrRoomId } from '../utils/matrix';
|
|
import {
|
|
getDirectRoomPath,
|
|
getHomeRoomPath,
|
|
getSpacePath,
|
|
getSpaceRoomPath,
|
|
} from '../pages/pathUtils';
|
|
import { useMatrixClient } from './useMatrixClient';
|
|
import { getOrphanParents, guessPerfectParent, isDirectStreamRoom } from '../utils/room';
|
|
import { roomToParentsAtom } from '../state/room/roomToParents';
|
|
import { mDirectAtom } from '../state/mDirectList';
|
|
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 mDirects = useAtomValue(mDirectAtom);
|
|
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);
|
|
const openSpaceTimeline = developerTools && spaceSelectedId === roomId;
|
|
|
|
const orphanParents = openSpaceTimeline ? [roomId] : 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(
|
|
getSpaceRoomPath(pSpaceIdOrAlias, openSpaceTimeline ? roomId : roomIdOrAlias, eventId),
|
|
opts
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Mirror the four-source DM gate used by useIsDirectStream and
|
|
// HomeRouteRoomProvider so imperative navigation (push tap, search
|
|
// result, inbox click) sends DMs to /direct/ even on the first frame
|
|
// before mDirectAtom has hydrated. Fall back to the simple atom check
|
|
// when the SDK doesn't know the room yet (e.g. peeking).
|
|
const targetRoom = mx.getRoom(roomId);
|
|
const isDirect = targetRoom
|
|
? isDirectStreamRoom(mx, targetRoom, mDirects)
|
|
: mDirects.has(roomId);
|
|
if (isDirect) {
|
|
safeNavigate(getDirectRoomPath(roomIdOrAlias, eventId), opts);
|
|
return;
|
|
}
|
|
|
|
safeNavigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
|
|
},
|
|
[mx, safeNavigate, spaceSelectedId, roomToParents, mDirects, developerTools]
|
|
);
|
|
|
|
return {
|
|
navigateSpace,
|
|
navigateRoom,
|
|
};
|
|
};
|