Open DMs during sync catch-up instead of blocking on parent maps.

Prefer the direct route for m.direct rooms and allow joined space children before roomToParents finishes backfilling.
This commit is contained in:
2026-07-24 16:52:34 +10:00
parent f62200ecd1
commit 99a294791e
5 changed files with 74 additions and 18 deletions

View File

@@ -56,6 +56,19 @@ export const useRoomNavigate = () => {
} }
const orphanParents = openSpaceTimeline ? [roomId] : getOrphanParents(roomToParents, roomId); const orphanParents = openSpaceTimeline ? [roomId] : getOrphanParents(roomToParents, roomId);
// Prefer Direct for m.direct rooms unless we are already browsing a parent space
// of this room (keep space chrome). Opening via space while roomToParents is still
// catching up used to hit JoinBeforeNavigate even though the DM was listed.
if (mDirects.has(roomId)) {
const stayInSelectedSpace =
Boolean(spaceSelectedId) && orphanParents.includes(spaceSelectedId!);
if (!stayInSelectedSpace) {
navigate(getDirectRoomPath(roomIdOrAlias, eventId), opts);
return;
}
}
if (orphanParents.length > 0) { if (orphanParents.length > 0) {
let parentSpace: string; let parentSpace: string;
if (spaceSelectedId && orphanParents.includes(spaceSelectedId)) { if (spaceSelectedId && orphanParents.includes(spaceSelectedId)) {
@@ -73,11 +86,6 @@ export const useRoomNavigate = () => {
return; return;
} }
if (mDirects.has(roomId)) {
navigate(getDirectRoomPath(roomIdOrAlias, eventId), opts);
return;
}
navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts); navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
}, },
[ [

View File

@@ -1,15 +1,19 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useAtomValue } from 'jotai';
import { decodeRouteParam } from '../../pathUtils'; import { decodeRouteParam } from '../../pathUtils';
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom'; import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { JoinBeforeNavigate } from '../../../features/join-before-navigate'; import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
import { useDirectRooms } from './useDirectRooms'; import { useDirectRooms } from './useDirectRooms';
import { mDirectAtom } from '../../../state/mDirectList';
import { Membership } from '../../../../types/matrix/room';
export function DirectRouteRoomProvider({ children }: { children: ReactNode }) { export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const rooms = useDirectRooms(); const rooms = useDirectRooms();
const mDirects = useAtomValue(mDirectAtom);
const { roomIdOrAlias: rawRoomIdOrAlias, eventId: rawEventId } = useParams(); const { roomIdOrAlias: rawRoomIdOrAlias, eventId: rawEventId } = useParams();
const roomIdOrAlias = decodeRouteParam(rawRoomIdOrAlias); const roomIdOrAlias = decodeRouteParam(rawRoomIdOrAlias);
@@ -17,7 +21,13 @@ export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
const roomId = useSelectedRoom(); const roomId = useSelectedRoom();
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
if (!room || !rooms.includes(room.roomId)) { // useDirectRooms can lag m.direct / allRooms during catch-up; allow joined m.direct rooms.
const isJoinedDirect =
Boolean(room) &&
room!.getMyMembership() === Membership.Join &&
(rooms.includes(room!.roomId) || mDirects.has(room!.roomId));
if (!room || !isJoinedDirect) {
return ( return (
<JoinBeforeNavigate <JoinBeforeNavigate
roomIdOrAlias={roomIdOrAlias ?? rawRoomIdOrAlias!} roomIdOrAlias={roomIdOrAlias ?? rawRoomIdOrAlias!}

View File

@@ -1,4 +1,4 @@
import React, { ReactNode } from 'react'; import React, { ReactNode, useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { decodeRouteParam } from '../../pathUtils'; import { decodeRouteParam } from '../../pathUtils';
@@ -30,7 +30,31 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
const roomId = useSelectedRoom(); const roomId = useSelectedRoom();
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
if (!room || !allRooms.includes(room.roomId)) { const isJoined = Boolean(room && allRooms.includes(room.roomId));
const isSpaceChild = Boolean(room && getSpaceChildren(space).includes(room.roomId));
const hasParentMapping = Boolean(
room && getAllParents(roomToParents, room.roomId).has(space.roomId)
);
// roomToParents can lag behind live space state during catch-up sync.
// If the room is clearly a child of this space, backfill the map and open it.
useEffect(() => {
if (!room || !isJoined || hasParentMapping || !isSpaceChild) return;
setRoomToParents({
type: 'PUT',
parent: space.roomId,
children: [room.roomId],
});
}, [
room,
isJoined,
hasParentMapping,
isSpaceChild,
space.roomId,
setRoomToParents,
]);
if (!room || !isJoined) {
// room is not joined // room is not joined
return ( return (
<JoinBeforeNavigate <JoinBeforeNavigate
@@ -50,16 +74,7 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
); );
} }
if (!getAllParents(roomToParents, room.roomId).has(space.roomId)) { if (!hasParentMapping && !isSpaceChild) {
if (getSpaceChildren(space).includes(room.roomId)) {
// fill missing roomToParent mapping
setRoomToParents({
type: 'PUT',
parent: space.roomId,
children: [room.roomId],
});
}
return ( return (
<JoinBeforeNavigate <JoinBeforeNavigate
roomIdOrAlias={roomIdOrAlias ?? rawRoomIdOrAlias!} roomIdOrAlias={roomIdOrAlias ?? rawRoomIdOrAlias!}

View File

@@ -103,6 +103,23 @@ export const useBindRoomToParentsAtom = (
setRoomToParents({ type: 'DELETE', roomId: childId }); setRoomToParents({ type: 'DELETE', roomId: childId });
} }
} }
return;
}
// Create type can arrive after the room is already in the store. Without this,
// spaces are skipped on INITIALIZE (isSpace false) and children stay unmapped
// until an unrelated SpaceChild event fires — DMs/channels look listed but
// SpaceRouteRoomProvider blocks on missing roomToParents.
if (mEvent.getType() === StateEvent.RoomCreate) {
const roomId = mEvent.getRoomId();
const room = roomId ? mx.getRoom(roomId) : null;
if (room && isSpace(room) && room.getMyMembership() !== Membership.Invite) {
setRoomToParents({
type: 'PUT',
parent: room.roomId,
children: getSpaceChildren(room),
});
}
} }
}; };

View File

@@ -887,6 +887,12 @@ body.stationery-dark-theme {
width: calc(100% + 25px) !important; width: calc(100% + 25px) !important;
max-width: none !important; max-width: none !important;
box-sizing: border-box !important; box-sizing: border-box !important;
/* Gutter is only for post-it overflow paint — do not steal clicks from page-nav */
pointer-events: none;
}
.stationery [data-sidebar-scroll] > * {
pointer-events: auto;
} }
.stationery [data-sidebar] > *, .stationery [data-sidebar] > *,