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); + }) + ); + } + } +);