Fix space navigation & view space timeline dev-option (#2358)

* fix inaccessible space on alias change

* fix new room in space open in home

* allow opening space timeline

* hide event timeline feature behind dev tool

* add navToActivePath to clear cache function
This commit is contained in:
Ajay Bura 2025-06-10 10:14:17 +05:30 committed by GitHub
parent e6f4eeca8e
commit 91632aa193
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 81 additions and 15 deletions

View file

@ -13,6 +13,8 @@ import { getOrphanParents } from '../utils/room';
import { roomToParentsAtom } from '../state/room/roomToParents'; import { roomToParentsAtom } from '../state/room/roomToParents';
import { mDirectAtom } from '../state/mDirectList'; import { mDirectAtom } from '../state/mDirectList';
import { useSelectedSpace } from './router/useSelectedSpace'; import { useSelectedSpace } from './router/useSelectedSpace';
import { settingsAtom } from '../state/settings';
import { useSetting } from '../state/hooks/settings';
export const useRoomNavigate = () => { export const useRoomNavigate = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -20,6 +22,7 @@ export const useRoomNavigate = () => {
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
const spaceSelectedId = useSelectedSpace(); const spaceSelectedId = useSelectedSpace();
const [developerTools] = useSetting(settingsAtom, 'developerTools');
const navigateSpace = useCallback( const navigateSpace = useCallback(
(roomId: string) => { (roomId: string) => {
@ -32,15 +35,22 @@ export const useRoomNavigate = () => {
const navigateRoom = useCallback( const navigateRoom = useCallback(
(roomId: string, eventId?: string, opts?: NavigateOptions) => { (roomId: string, eventId?: string, opts?: NavigateOptions) => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId); const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
const openSpaceTimeline = developerTools && spaceSelectedId === roomId;
const orphanParents = getOrphanParents(roomToParents, roomId); const orphanParents = openSpaceTimeline ? [roomId] : getOrphanParents(roomToParents, roomId);
if (orphanParents.length > 0) { if (orphanParents.length > 0) {
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId( const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
mx, mx,
spaceSelectedId && orphanParents.includes(spaceSelectedId) spaceSelectedId && orphanParents.includes(spaceSelectedId)
? spaceSelectedId ? spaceSelectedId
: orphanParents[0] : orphanParents[0] // TODO: better orphan parent selection.
); );
if (openSpaceTimeline) {
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomId, eventId), opts);
return;
}
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts); navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
return; return;
} }
@ -52,7 +62,7 @@ export const useRoomNavigate = () => {
navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts); navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
}, },
[mx, navigate, spaceSelectedId, roomToParents, mDirects] [mx, navigate, spaceSelectedId, roomToParents, mDirects, developerTools]
); );
return { return {

View file

@ -744,13 +744,14 @@ export function SpaceTabs({ scrollRef }: SpaceTabsProps) {
const targetSpaceId = target.getAttribute('data-id'); const targetSpaceId = target.getAttribute('data-id');
if (!targetSpaceId) return; if (!targetSpaceId) return;
const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, targetSpaceId));
if (screenSize === ScreenSize.Mobile) { if (screenSize === ScreenSize.Mobile) {
navigate(getSpacePath(getCanonicalAliasOrRoomId(mx, targetSpaceId))); navigate(spacePath);
return; return;
} }
const activePath = navToActivePath.get(targetSpaceId); const activePath = navToActivePath.get(targetSpaceId);
if (activePath) { if (activePath && activePath.pathname.startsWith(spacePath)) {
navigate(joinPathComponent(activePath)); navigate(joinPathComponent(activePath));
return; return;
} }

View file

@ -1,21 +1,24 @@
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 { useAtom, useAtomValue } from 'jotai';
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 { useSpace } from '../../../hooks/useSpace'; import { useSpace } from '../../../hooks/useSpace';
import { getAllParents } from '../../../utils/room'; import { getAllParents, getSpaceChildren } from '../../../utils/room';
import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { allRoomsAtom } from '../../../state/room-list/roomList'; import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers'; import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
import { mDirectAtom } from '../../../state/mDirectList'; import { mDirectAtom } from '../../../state/mDirectList';
import { settingsAtom } from '../../../state/settings';
import { useSetting } from '../../../state/hooks/settings';
export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) { export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const space = useSpace(); const space = useSpace();
const roomToParents = useAtomValue(roomToParentsAtom); const [developerTools] = useSetting(settingsAtom, 'developerTools');
const [roomToParents, setRoomToParents] = useAtom(roomToParentsAtom);
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
@ -24,12 +27,36 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
const roomId = useSelectedRoom(); const roomId = useSelectedRoom();
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
if ( if (!room || !allRooms.includes(room.roomId)) {
!room || // room is not joined
room.isSpaceRoom() || return (
!allRooms.includes(room.roomId) || <JoinBeforeNavigate
!getAllParents(roomToParents, room.roomId).has(space.roomId) roomIdOrAlias={roomIdOrAlias!}
) { eventId={eventId}
viaServers={viaServers}
/>
);
}
if (developerTools && room.isSpaceRoom() && room.roomId === space.roomId) {
// allow to view space timeline
return (
<RoomProvider key={room.roomId} value={room}>
<IsDirectRoomProvider value={mDirects.has(room.roomId)}>{children}</IsDirectRoomProvider>
</RoomProvider>
);
}
if (!getAllParents(roomToParents, room.roomId).has(space.roomId)) {
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!} roomIdOrAlias={roomIdOrAlias!}

View file

@ -75,6 +75,7 @@ import {
useRoomsNotificationPreferencesContext, useRoomsNotificationPreferencesContext,
} from '../../../hooks/useRoomsNotificationPreferences'; } from '../../../hooks/useRoomsNotificationPreferences';
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings'; import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
type SpaceMenuProps = { type SpaceMenuProps = {
room: Room; room: Room;
@ -83,11 +84,13 @@ type SpaceMenuProps = {
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClose }, ref) => { const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClose }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const [developerTools] = useSetting(settingsAtom, 'developerTools');
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const openSpaceSettings = useOpenSpaceSettings(); const openSpaceSettings = useOpenSpaceSettings();
const { navigateRoom } = useRoomNavigate();
const allChild = useSpaceChildren( const allChild = useSpaceChildren(
allRoomsAtom, allRoomsAtom,
@ -118,6 +121,11 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
requestClose(); requestClose();
}; };
const handleOpenTimeline = () => {
navigateRoom(room.roomId);
requestClose();
};
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
@ -168,6 +176,18 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
Space Settings Space Settings
</Text> </Text>
</MenuItem> </MenuItem>
{developerTools && (
<MenuItem
onClick={handleOpenTimeline}
size="300"
after={<Icon size="100" src={Icons.Terminal} />}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Event Timeline
</Text>
</MenuItem>
)}
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>

View file

@ -9,6 +9,8 @@ import {
const NAV_TO_ACTIVE_PATH = 'navToActivePath'; const NAV_TO_ACTIVE_PATH = 'navToActivePath';
const getStoreKey = (userId: string): string => `${NAV_TO_ACTIVE_PATH}${userId}`;
type NavToActivePath = Map<string, Path>; type NavToActivePath = Map<string, Path>;
type NavToActivePathAction = type NavToActivePathAction =
@ -25,7 +27,7 @@ type NavToActivePathAction =
export type NavToActivePathAtom = WritableAtom<NavToActivePath, [NavToActivePathAction], undefined>; export type NavToActivePathAtom = WritableAtom<NavToActivePath, [NavToActivePathAction], undefined>;
export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom => { export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom => {
const storeKey = `${NAV_TO_ACTIVE_PATH}${userId}`; const storeKey = getStoreKey(userId);
const baseNavToActivePathAtom = atomWithLocalStorage<NavToActivePath>( const baseNavToActivePathAtom = atomWithLocalStorage<NavToActivePath>(
storeKey, storeKey,
@ -64,3 +66,7 @@ export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom =>
return navToActivePathAtom; return navToActivePathAtom;
}; };
export const clearNavToActivePathStore = (userId: string) => {
localStorage.removeItem(getStoreKey(userId));
};

View file

@ -1,6 +1,7 @@
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk'; import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
import { cryptoCallbacks } from './state/secretStorageKeys'; import { cryptoCallbacks } from './state/secretStorageKeys';
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
type Session = { type Session = {
baseUrl: string; baseUrl: string;
@ -46,6 +47,7 @@ export const startClient = async (mx: MatrixClient) => {
export const clearCacheAndReload = async (mx: MatrixClient) => { export const clearCacheAndReload = async (mx: MatrixClient) => {
mx.stopClient(); mx.stopClient();
clearNavToActivePathStore(mx.getSafeUserId());
await mx.store.deleteAllData(); await mx.store.deleteAllData();
window.location.reload(); window.location.reload();
}; };