diff --git a/src/app/hooks/navToActivePathMapper.ts b/src/app/hooks/navToActivePathMapper.ts
new file mode 100644
index 00000000..6f041856
--- /dev/null
+++ b/src/app/hooks/navToActivePathMapper.ts
@@ -0,0 +1,18 @@
+import { useEffect } from 'react';
+import { useSetAtom } from 'jotai';
+import { useLocation } from 'react-router-dom';
+import { navToActivePathAtom } from '../state/navToActivePath';
+
+export const useNavToActivePathMapper = (navId: string) => {
+ const location = useLocation();
+ const setNavToActivePath = useSetAtom(navToActivePathAtom);
+
+ useEffect(() => {
+ const { pathname, search, hash } = location;
+ setNavToActivePath({
+ type: 'PUT',
+ navId,
+ path: { pathname, search, hash },
+ });
+ }, [location, setNavToActivePath, navId]);
+};
diff --git a/src/app/hooks/useNavCategoryHandler.ts b/src/app/hooks/useNavCategoryHandler.ts
index f4407fa3..062e909f 100644
--- a/src/app/hooks/useNavCategoryHandler.ts
+++ b/src/app/hooks/useNavCategoryHandler.ts
@@ -1,9 +1,9 @@
import { useSetAtom } from 'jotai';
import { MouseEventHandler } from 'react';
-import { closedNavCategories } from '../state/closedNavCategories';
+import { closedNavCategoriesAtom } from '../state/closedNavCategories';
export const useNavCategoryHandler = (closed: (categoryId: string) => boolean) => {
- const setClosedCategory = useSetAtom(closedNavCategories);
+ const setClosedCategory = useSetAtom(closedNavCategoriesAtom);
const handleCategoryClick: MouseEventHandler = (evt) => {
const categoryId = evt.currentTarget.getAttribute('data-category-id');
diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx
index f3012cee..60807629 100644
--- a/src/app/pages/App.tsx
+++ b/src/app/pages/App.tsx
@@ -42,7 +42,7 @@ import { ClientLayout, ClientRoot } from './client';
import { Home, HomeSearch } from './client/home';
import { RoomViewer } from '../organisms/room/Room';
import { Direct } from './client/direct';
-import { RouteSpaceProvider, Space, SpaceSearch } from './client/space';
+import { RouteSpaceProvider, Space, SpaceIndexRedirect, SpaceSearch } from './client/space';
import { Explore, ExploreRedirect, FeaturedRooms, PublicRooms } from './client/explore';
import { Notifications, Inbox, InboxRedirect, Invites } from './client/inbox';
import { setAfterLoginRedirectPath } from './afterLoginRedirectPath';
@@ -99,7 +99,7 @@ const createRouter = (clientConfig: ClientConfig) => {
}>
}>
- welcome
} />
+ } />
lobby} />
} />
} />
diff --git a/src/app/pages/client/direct/Direct.tsx b/src/app/pages/client/direct/Direct.tsx
index 9873060b..a9720aa1 100644
--- a/src/app/pages/client/direct/Direct.tsx
+++ b/src/app/pages/client/direct/Direct.tsx
@@ -28,7 +28,7 @@ import { useDirectCreateSelected } from '../../../hooks/router/useDirectSelected
import { VirtualTile } from '../../../components/virtualizer';
import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
import { muteChangesAtom } from '../../../state/room-list/mutedRoomList';
-import { closedNavCategories, makeNavCategoryId } from '../../../state/closedNavCategories';
+import { closedNavCategoriesAtom, makeNavCategoryId } from '../../../state/closedNavCategories';
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
import { useNavCategoryHandler } from '../../../hooks/useNavCategoryHandler';
@@ -72,7 +72,7 @@ export function Direct() {
const selectedRoomId = useSelectedRoom();
const createSelected = useDirectCreateSelected();
const noRoomToDisplay = directs.length === 0;
- const closedCategories = useAtomValue(closedNavCategories);
+ const closedCategories = useAtomValue(closedNavCategoriesAtom);
const sortedDirects = useMemo(() => {
const items = Array.from(directs).sort(factoryRoomIdByActivity(mx));
diff --git a/src/app/pages/client/home/Home.tsx b/src/app/pages/client/home/Home.tsx
index 542ac05b..d0332ce3 100644
--- a/src/app/pages/client/home/Home.tsx
+++ b/src/app/pages/client/home/Home.tsx
@@ -36,7 +36,7 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { VirtualTile } from '../../../components/virtualizer';
import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
import { muteChangesAtom } from '../../../state/room-list/mutedRoomList';
-import { closedNavCategories, makeNavCategoryId } from '../../../state/closedNavCategories';
+import { closedNavCategoriesAtom, makeNavCategoryId } from '../../../state/closedNavCategories';
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
import { useNavCategoryHandler } from '../../../hooks/useNavCategoryHandler';
@@ -95,7 +95,7 @@ export function Home() {
const joinSelected = useHomeJoinSelected();
const searchSelected = useHomeSearchSelected();
const noRoomToDisplay = rooms.length === 0;
- const closedCategories = useAtomValue(closedNavCategories);
+ const closedCategories = useAtomValue(closedNavCategoriesAtom);
const sortedRooms = useMemo(() => {
const items = Array.from(rooms).sort(factoryRoomIdByAtoZ(mx));
diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx
index 8fdda448..e2f3c39d 100644
--- a/src/app/pages/client/sidebar/SpaceTabs.tsx
+++ b/src/app/pages/client/sidebar/SpaceTabs.tsx
@@ -6,19 +6,21 @@ import { useOrphanSpaces } from '../../../state/hooks/roomList';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { allRoomsAtom } from '../../../state/room-list/roomList';
-import { getSpacePath, getSpaceRoomPath } from '../../pathUtils';
+import { getSpacePath, getSpaceRoomPath, joinPathComponent } from '../../pathUtils';
import { SidebarAvatar } from '../../../components/sidebar';
import { NotificationBadge, UnreadMenu } from './NotificationBadge';
import { RoomUnreadProvider } from '../../../components/RoomUnreadProvider';
import colorMXID from '../../../../util/colorMXID';
import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace';
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
+import { navToActivePathAtom } from '../../../state/navToActivePath';
export function SpaceTabs() {
const navigate = useNavigate();
const mx = useMatrixClient();
const roomToParents = useAtomValue(roomToParentsAtom);
const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, roomToParents);
+ const navToActivePath = useAtomValue(navToActivePathAtom);
const selectedSpaceId = useSelectedSpace();
@@ -30,6 +32,12 @@ export function SpaceTabs() {
const targetSpaceId = target.getAttribute('data-id');
if (!targetSpaceId) return;
+ const activePath = navToActivePath.get(targetSpaceId);
+ if (activePath) {
+ navigate(joinPathComponent(activePath));
+ return;
+ }
+
const targetSpaceAlias = mx.getRoom(targetSpaceId)?.getCanonicalAlias();
navigate(getSpacePath(targetSpaceAlias ?? targetSpaceId));
};
diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx
index 352887c5..ff8e5bf1 100644
--- a/src/app/pages/client/space/Space.tsx
+++ b/src/app/pages/client/space/Space.tsx
@@ -28,13 +28,15 @@ import { VirtualTile } from '../../../components/virtualizer';
import { useSpaceHierarchy } from './useSpaceHierarchy';
import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
import { muteChangesAtom } from '../../../state/room-list/mutedRoomList';
-import { closedNavCategories, makeNavCategoryId } from '../../../state/closedNavCategories';
+import { closedNavCategoriesAtom, makeNavCategoryId } from '../../../state/closedNavCategories';
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
import { useNavCategoryHandler } from '../../../hooks/useNavCategoryHandler';
+import { useNavToActivePathMapper } from '../../../hooks/navToActivePathMapper';
export function Space() {
const mx = useMatrixClient();
const space = useSpace();
+ useNavToActivePathMapper(space.roomId);
const spaceIdOrAlias = getCanonicalAliasOrRoomId(mx, space.roomId);
const scrollRef = useRef(null);
const mDirects = useAtomValue(mDirectAtom);
@@ -46,7 +48,7 @@ export function Space() {
const lobbySelected = useSpaceLobbySelected(spaceIdOrAlias);
const searchSelected = useSpaceSearchSelected(spaceIdOrAlias);
- const closedCategories = useAtomValue(closedNavCategories);
+ const closedCategories = useAtomValue(closedNavCategoriesAtom);
const hierarchy = useSpaceHierarchy(
space.roomId,
useCallback(
diff --git a/src/app/pages/client/space/SpaceIndexRedirect.tsx b/src/app/pages/client/space/SpaceIndexRedirect.tsx
new file mode 100644
index 00000000..50ed8a80
--- /dev/null
+++ b/src/app/pages/client/space/SpaceIndexRedirect.tsx
@@ -0,0 +1,26 @@
+import { useAtomValue } from 'jotai';
+import { useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { navToActivePathAtom } from '../../../state/navToActivePath';
+import { useSpace } from '../../../hooks/useSpace';
+import { getSpaceLobbyPath, joinPathComponent } from '../../pathUtils';
+import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+
+export function SpaceIndexRedirect() {
+ const navigate = useNavigate();
+ const mx = useMatrixClient();
+ const space = useSpace();
+ const navToActivePath = useAtomValue(navToActivePathAtom);
+
+ useEffect(() => {
+ const activePath = navToActivePath.get(space.roomId);
+ if (activePath) {
+ navigate(joinPathComponent(activePath), { replace: true });
+ } else {
+ navigate(getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, space.roomId)), { replace: true });
+ }
+ }, [navigate, mx, space.roomId, navToActivePath]);
+
+ return null;
+}
diff --git a/src/app/pages/client/space/index.ts b/src/app/pages/client/space/index.ts
index 57afadc4..ae665547 100644
--- a/src/app/pages/client/space/index.ts
+++ b/src/app/pages/client/space/index.ts
@@ -1,3 +1,4 @@
export * from './SpaceProvider';
export * from './Space';
+export * from './SpaceIndexRedirect';
export * from './Search';
diff --git a/src/app/pages/pathUtils.ts b/src/app/pages/pathUtils.ts
index fae2ba82..43c07d28 100644
--- a/src/app/pages/pathUtils.ts
+++ b/src/app/pages/pathUtils.ts
@@ -1,4 +1,4 @@
-import { generatePath } from 'react-router-dom';
+import { generatePath, Path } from 'react-router-dom';
import {
DIRECT_CREATE_PATH,
DIRECT_PATH,
@@ -25,6 +25,8 @@ import {
} from './paths';
import { trimTrailingSlash } from '../utils/common';
+export const joinPathComponent = (path: Path): string => path.pathname + path.search + path.hash;
+
export const withSearchParam = >(
path: string,
searchParam: T
diff --git a/src/app/state/closedNavCategories.ts b/src/app/state/closedNavCategories.ts
index 7d6da412..416e21d0 100644
--- a/src/app/state/closedNavCategories.ts
+++ b/src/app/state/closedNavCategories.ts
@@ -8,7 +8,7 @@ import {
const CLOSED_NAV_CATEGORY = 'closedNavCategories';
-const baseClosedNavCategories = atomWithLocalStorage>(
+const baseClosedNavCategoriesAtom = atomWithLocalStorage>(
CLOSED_NAV_CATEGORY,
(key) => {
const arrayValue = getLocalStorageItem(key, []);
@@ -30,13 +30,13 @@ type ClosedNavCategoriesAction =
categoryId: string;
};
-export const closedNavCategories = atom, [ClosedNavCategoriesAction], undefined>(
- (get) => get(baseClosedNavCategories),
+export const closedNavCategoriesAtom = atom, [ClosedNavCategoriesAction], undefined>(
+ (get) => get(baseClosedNavCategoriesAtom),
(get, set, action) => {
if (action.type === 'DELETE') {
set(
- baseClosedNavCategories,
- produce(get(baseClosedNavCategories), (draft) => {
+ baseClosedNavCategoriesAtom,
+ produce(get(baseClosedNavCategoriesAtom), (draft) => {
draft.delete(action.categoryId);
})
);
@@ -44,8 +44,8 @@ export const closedNavCategories = atom, [ClosedNavCategoriesAction]
}
if (action.type === 'PUT') {
set(
- baseClosedNavCategories,
- produce(get(baseClosedNavCategories), (draft) => {
+ baseClosedNavCategoriesAtom,
+ produce(get(baseClosedNavCategoriesAtom), (draft) => {
draft.add(action.categoryId);
})
);
diff --git a/src/app/state/navToActivePath.ts b/src/app/state/navToActivePath.ts
new file mode 100644
index 00000000..5b3b63e8
--- /dev/null
+++ b/src/app/state/navToActivePath.ts
@@ -0,0 +1,57 @@
+import { atom } from 'jotai';
+import produce from 'immer';
+import { Path } from 'react-router-dom';
+import {
+ atomWithLocalStorage,
+ getLocalStorageItem,
+ setLocalStorageItem,
+} from './utils/atomWithLocalStorage';
+
+const NAV_TO_ACTIVE_PATH = 'navToActivePath';
+
+type NavToActivePath = Map;
+
+const baseNavToActivePathAtom = atomWithLocalStorage(
+ NAV_TO_ACTIVE_PATH,
+ (key) => {
+ const obj: Record = getLocalStorageItem(key, {});
+ return new Map(Object.entries(obj));
+ },
+ (key, value) => {
+ const obj: Record = Object.fromEntries(value);
+ setLocalStorageItem(key, obj);
+ }
+);
+
+type NavToActivePathAction =
+ | {
+ type: 'PUT';
+ navId: string;
+ path: Path;
+ }
+ | {
+ type: 'DELETE';
+ navId: string;
+ };
+export const navToActivePathAtom = atom(
+ (get) => get(baseNavToActivePathAtom),
+ (get, set, action) => {
+ if (action.type === 'DELETE') {
+ set(
+ baseNavToActivePathAtom,
+ produce(get(baseNavToActivePathAtom), (draft) => {
+ draft.delete(action.navId);
+ })
+ );
+ return;
+ }
+ if (action.type === 'PUT') {
+ set(
+ baseNavToActivePathAtom,
+ produce(get(baseNavToActivePathAtom), (draft) => {
+ draft.set(action.navId, action.path);
+ })
+ );
+ }
+ }
+);