Fix space navigation & view space timeline dev-option (#2358)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Tue, 10 Jun 2025 04:44:17 +0000 (10:14 +0530)
committerGitHub <noreply@github.com>
Tue, 10 Jun 2025 04:44:17 +0000 (14:44 +1000)
* 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

src/app/hooks/useRoomNavigate.ts
src/app/pages/client/sidebar/SpaceTabs.tsx
src/app/pages/client/space/RoomProvider.tsx
src/app/pages/client/space/Space.tsx
src/app/state/navToActivePath.ts
src/client/initMatrix.ts

index 0f9f365cd4947b97d4f80e77b6e5938b795fa6d7..e626c06bff74eda83d7d900e1c4699f4c5129190 100644 (file)
@@ -13,6 +13,8 @@ import { getOrphanParents } 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();
@@ -20,6 +22,7 @@ export const useRoomNavigate = () => {
   const roomToParents = useAtomValue(roomToParentsAtom);
   const mDirects = useAtomValue(mDirectAtom);
   const spaceSelectedId = useSelectedSpace();
+  const [developerTools] = useSetting(settingsAtom, 'developerTools');
 
   const navigateSpace = useCallback(
     (roomId: string) => {
@@ -32,15 +35,22 @@ export const useRoomNavigate = () => {
   const navigateRoom = useCallback(
     (roomId: string, eventId?: string, opts?: NavigateOptions) => {
       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) {
         const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
           mx,
           spaceSelectedId && orphanParents.includes(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);
         return;
       }
@@ -52,7 +62,7 @@ export const useRoomNavigate = () => {
 
       navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
     },
-    [mx, navigate, spaceSelectedId, roomToParents, mDirects]
+    [mx, navigate, spaceSelectedId, roomToParents, mDirects, developerTools]
   );
 
   return {
index 5b47cb52ceca4b2448e6ff3b125fbf13f0e829d3..011741eee24435a77a5389ec4b4e4dec25a957e0 100644 (file)
@@ -744,13 +744,14 @@ export function SpaceTabs({ scrollRef }: SpaceTabsProps) {
     const targetSpaceId = target.getAttribute('data-id');
     if (!targetSpaceId) return;
 
+    const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, targetSpaceId));
     if (screenSize === ScreenSize.Mobile) {
-      navigate(getSpacePath(getCanonicalAliasOrRoomId(mx, targetSpaceId)));
+      navigate(spacePath);
       return;
     }
 
     const activePath = navToActivePath.get(targetSpaceId);
-    if (activePath) {
+    if (activePath && activePath.pathname.startsWith(spacePath)) {
       navigate(joinPathComponent(activePath));
       return;
     }
index a963213740434c2ad96c176d3a1bdd9d291bff36..0fd52ab670ec5f1376302cd8e670774bcdf44927 100644 (file)
@@ -1,21 +1,24 @@
 import React, { ReactNode } from 'react';
 import { useParams } from 'react-router-dom';
-import { useAtomValue } from 'jotai';
+import { useAtom, useAtomValue } from 'jotai';
 import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
 import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
 import { useMatrixClient } from '../../../hooks/useMatrixClient';
 import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
 import { useSpace } from '../../../hooks/useSpace';
-import { getAllParents } from '../../../utils/room';
+import { getAllParents, getSpaceChildren } from '../../../utils/room';
 import { roomToParentsAtom } from '../../../state/room/roomToParents';
 import { allRoomsAtom } from '../../../state/room-list/roomList';
 import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
 import { mDirectAtom } from '../../../state/mDirectList';
+import { settingsAtom } from '../../../state/settings';
+import { useSetting } from '../../../state/hooks/settings';
 
 export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
   const mx = useMatrixClient();
   const space = useSpace();
-  const roomToParents = useAtomValue(roomToParentsAtom);
+  const [developerTools] = useSetting(settingsAtom, 'developerTools');
+  const [roomToParents, setRoomToParents] = useAtom(roomToParentsAtom);
   const mDirects = useAtomValue(mDirectAtom);
   const allRooms = useAtomValue(allRoomsAtom);
 
@@ -24,12 +27,36 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
   const roomId = useSelectedRoom();
   const room = mx.getRoom(roomId);
 
-  if (
-    !room ||
-    room.isSpaceRoom() ||
-    !allRooms.includes(room.roomId) ||
-    !getAllParents(roomToParents, room.roomId).has(space.roomId)
-  ) {
+  if (!room || !allRooms.includes(room.roomId)) {
+    // room is not joined
+    return (
+      <JoinBeforeNavigate
+        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 (
       <JoinBeforeNavigate
         roomIdOrAlias={roomIdOrAlias!}
index fa8c0ea8a0ced54ec66282153c2568d0c6ca1deb..d100946469053df75ac2cd45e086b2d3217dbf41 100644 (file)
@@ -75,6 +75,7 @@ import {
   useRoomsNotificationPreferencesContext,
 } from '../../../hooks/useRoomsNotificationPreferences';
 import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
+import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
 
 type SpaceMenuProps = {
   room: Room;
@@ -83,11 +84,13 @@ type SpaceMenuProps = {
 const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClose }, ref) => {
   const mx = useMatrixClient();
   const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+  const [developerTools] = useSetting(settingsAtom, 'developerTools');
   const roomToParents = useAtomValue(roomToParentsAtom);
   const powerLevels = usePowerLevels(room);
   const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
   const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
   const openSpaceSettings = useOpenSpaceSettings();
+  const { navigateRoom } = useRoomNavigate();
 
   const allChild = useSpaceChildren(
     allRoomsAtom,
@@ -118,6 +121,11 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
     requestClose();
   };
 
+  const handleOpenTimeline = () => {
+    navigateRoom(room.roomId);
+    requestClose();
+  };
+
   return (
     <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
       <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
@@ -168,6 +176,18 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
             Space Settings
           </Text>
         </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>
       <Line variant="Surface" size="300" />
       <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
index 80869146add35b628da64dcf0d4b9971c003ecc5..af90c914b0a2dad64fdca4440da0c910abd5a381 100644 (file)
@@ -9,6 +9,8 @@ import {
 
 const NAV_TO_ACTIVE_PATH = 'navToActivePath';
 
+const getStoreKey = (userId: string): string => `${NAV_TO_ACTIVE_PATH}${userId}`;
+
 type NavToActivePath = Map<string, Path>;
 
 type NavToActivePathAction =
@@ -25,7 +27,7 @@ type NavToActivePathAction =
 export type NavToActivePathAtom = WritableAtom<NavToActivePath, [NavToActivePathAction], undefined>;
 
 export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom => {
-  const storeKey = `${NAV_TO_ACTIVE_PATH}${userId}`;
+  const storeKey = getStoreKey(userId);
 
   const baseNavToActivePathAtom = atomWithLocalStorage<NavToActivePath>(
     storeKey,
@@ -64,3 +66,7 @@ export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom =>
 
   return navToActivePathAtom;
 };
+
+export const clearNavToActivePathStore = (userId: string) => {
+  localStorage.removeItem(getStoreKey(userId));
+};
index b513e27cb766a99b7b414fe571d849997ccad207..b80a080fbe087a27b25caf8fd0444323a97571de 100644 (file)
@@ -1,6 +1,7 @@
 import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
 
 import { cryptoCallbacks } from './state/secretStorageKeys';
+import { clearNavToActivePathStore } from '../app/state/navToActivePath';
 
 type Session = {
   baseUrl: string;
@@ -46,6 +47,7 @@ export const startClient = async (mx: MatrixClient) => {
 
 export const clearCacheAndReload = async (mx: MatrixClient) => {
   mx.stopClient();
+  clearNavToActivePathStore(mx.getSafeUserId());
   await mx.store.deleteAllData();
   window.location.reload();
 };