fix(channels): back active-workspace persistence with a jotai atom so the native pager sees switcher picks instead of a stale memoized localStorage read
This commit is contained in:
parent
f104bdfe8b
commit
4a2e29fdb5
3 changed files with 62 additions and 27 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { Icons } from 'folds';
|
||||
import { useSpace } from '../../../hooks/useSpace';
|
||||
import { PageNav, PageNavContent } from '../../../components/page';
|
||||
|
|
@ -8,12 +9,12 @@ import { useMobilePagerPane } from '../../../components/mobile-tabs-pager/Mobile
|
|||
import { StreamHeaderPrimaryAction } from '../../../state/mobilePagerHeader';
|
||||
import { useOpenCreateRoomModal } from '../../../state/hooks/createRoomModal';
|
||||
import { useOpenCreateSpaceModal } from '../../../state/hooks/createSpaceModal';
|
||||
import { activeChannelsSpaceAtom } from '../../../state/activeChannelsSpace';
|
||||
import { CreateRoomType } from '../../../components/create-room/types';
|
||||
import { ChannelsList } from './ChannelsList';
|
||||
import { ChannelsLanding } from './ChannelsLanding';
|
||||
import { ChannelsWorkspaceHorseshoe } from './ChannelsWorkspaceHorseshoe';
|
||||
import { WorkspaceFooter } from './WorkspaceFooter';
|
||||
import { ACTIVE_SPACE_KEY } from './useActiveSpace';
|
||||
|
||||
// Index route at /channels/ (no space selected). Renders the shared
|
||||
// StreamHeader (segment switcher) plus the resolve-active-space-or-
|
||||
|
|
@ -79,18 +80,17 @@ export function Channels() {
|
|||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inPagerMode = useMobilePagerPane() !== null;
|
||||
const openCreateRoomModal = useOpenCreateRoomModal();
|
||||
const setActiveSpace = useSetAtom(activeChannelsSpaceAtom);
|
||||
|
||||
// Persist URL-driven active space so cold-starts at /channels/ resume on
|
||||
// the same workspace. `useActiveSpace` (in ChannelsLanding) reads the
|
||||
// value but never writes it because the index route has no
|
||||
// :spaceIdOrAlias param — the write happens here.
|
||||
// the same workspace. `useActiveSpace` (in ChannelsLanding and
|
||||
// MobileTabsPager) subscribes to the same atom, so this write
|
||||
// immediately invalidates their cached reads — without that, the pager
|
||||
// would serve a stale `destinationFor('channels')` after a switcher
|
||||
// pick + tab swap on native.
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(ACTIVE_SPACE_KEY, space.roomId);
|
||||
} catch {
|
||||
/* private mode / quota — non-fatal */
|
||||
}
|
||||
}, [space.roomId]);
|
||||
setActiveSpace(space.roomId);
|
||||
}, [setActiveSpace, space.roomId]);
|
||||
|
||||
// Plus on the tabs row creates a channel inside the active workspace.
|
||||
// Single entry point for «create» on the Channels tab.
|
||||
|
|
|
|||
|
|
@ -1,18 +1,10 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { activeChannelsSpaceAtom } from '../../../state/activeChannelsSpace';
|
||||
import { getCanonicalAliasRoomId, isRoomAlias } from '../../../utils/matrix';
|
||||
|
||||
export const ACTIVE_SPACE_KEY = 'vojo.activeSpaceId';
|
||||
|
||||
const readPersisted = (): string | null => {
|
||||
try {
|
||||
return localStorage.getItem(ACTIVE_SPACE_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const safeDecode = (raw: string): string | undefined => {
|
||||
try {
|
||||
return decodeURIComponent(raw);
|
||||
|
|
@ -23,16 +15,22 @@ const safeDecode = (raw: string): string | undefined => {
|
|||
|
||||
// Resolves the active Space for the channels segment. Priority:
|
||||
// 1. URL `:spaceIdOrAlias` param (if joined-orphan)
|
||||
// 2. localStorage['vojo.activeSpaceId'] (if joined-orphan)
|
||||
// 2. `activeChannelsSpaceAtom` (= localStorage, reactive) (if joined-orphan)
|
||||
// 3. first joined-orphan Space
|
||||
// Returns undefined when the user has 0 joined orphan spaces. Persistence
|
||||
// (writing to localStorage) lives in `Channels.tsx`, where the inner
|
||||
// route already has the resolved space context — at the index `/channels/`
|
||||
// (writing the atom) lives in `Channels.tsx`, where the inner route
|
||||
// already has the resolved space context — at the index `/channels/`
|
||||
// route, useParams().spaceIdOrAlias is always undefined.
|
||||
//
|
||||
// The persisted value is read via Jotai so writes from `Channels.tsx`
|
||||
// immediately invalidate every consumer (notably `MobileTabsPager`,
|
||||
// which stays mounted across tab swipes and would otherwise serve a
|
||||
// stale cached value to its `destinationFor('channels')`).
|
||||
export const useActiveSpace = (orphanSpaceIds: string[]): string | undefined => {
|
||||
const mx = useMatrixClient();
|
||||
const { spaceIdOrAlias } = useParams();
|
||||
const orphanSet = useMemo(() => new Set(orphanSpaceIds), [orphanSpaceIds]);
|
||||
const persisted = useAtomValue(activeChannelsSpaceAtom);
|
||||
|
||||
const urlSpaceId = useMemo(() => {
|
||||
if (!spaceIdOrAlias) return undefined;
|
||||
|
|
@ -42,10 +40,10 @@ export const useActiveSpace = (orphanSpaceIds: string[]): string | undefined =>
|
|||
return resolved && orphanSet.has(resolved) ? resolved : undefined;
|
||||
}, [mx, spaceIdOrAlias, orphanSet]);
|
||||
|
||||
const persistedSpaceId = useMemo(() => {
|
||||
const stored = readPersisted();
|
||||
return stored && orphanSet.has(stored) ? stored : undefined;
|
||||
}, [orphanSet]);
|
||||
const persistedSpaceId = useMemo(
|
||||
() => (persisted && orphanSet.has(persisted) ? persisted : undefined),
|
||||
[persisted, orphanSet]
|
||||
);
|
||||
|
||||
return urlSpaceId ?? persistedSpaceId ?? orphanSpaceIds[0];
|
||||
};
|
||||
|
|
|
|||
37
src/app/state/activeChannelsSpace.ts
Normal file
37
src/app/state/activeChannelsSpace.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { atomWithLocalStorage } from './utils/atomWithLocalStorage';
|
||||
|
||||
// Persisted across reloads and tabs as a plain string (not JSON-encoded)
|
||||
// to stay backwards-compatible with the legacy `localStorage.setItem(key, roomId)`
|
||||
// writes that shipped before this atom existed.
|
||||
export const ACTIVE_CHANNELS_SPACE_KEY = 'vojo.activeSpaceId';
|
||||
|
||||
const getRawString = (key: string): string | undefined => {
|
||||
try {
|
||||
return localStorage.getItem(key) ?? undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const setRawString = (key: string, value: string | undefined) => {
|
||||
try {
|
||||
if (value === undefined) {
|
||||
localStorage.removeItem(key);
|
||||
} else {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
} catch {
|
||||
/* private mode / quota — non-fatal */
|
||||
}
|
||||
};
|
||||
|
||||
// Single source of truth for the channels-tab «active workspace» selection.
|
||||
// Subscribed reads (via `useAtomValue`) re-render when the atom is written
|
||||
// in the same tab and when another tab updates localStorage — fixes the
|
||||
// stale-memo bug where `MobileTabsPager` cached the persisted value at
|
||||
// first render and never refreshed after the user picked a new workspace.
|
||||
export const activeChannelsSpaceAtom = atomWithLocalStorage<string | undefined>(
|
||||
ACTIVE_CHANNELS_SPACE_KEY,
|
||||
getRawString,
|
||||
setRawString
|
||||
);
|
||||
Loading…
Add table
Reference in a new issue