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
765445c091
commit
7b3a4145a7
3 changed files with 62 additions and 27 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import { Icons } from 'folds';
|
import { Icons } from 'folds';
|
||||||
import { useSpace } from '../../../hooks/useSpace';
|
import { useSpace } from '../../../hooks/useSpace';
|
||||||
import { PageNav, PageNavContent } from '../../../components/page';
|
import { PageNav, PageNavContent } from '../../../components/page';
|
||||||
|
|
@ -8,12 +9,12 @@ import { useMobilePagerPane } from '../../../components/mobile-tabs-pager/Mobile
|
||||||
import { StreamHeaderPrimaryAction } from '../../../state/mobilePagerHeader';
|
import { StreamHeaderPrimaryAction } from '../../../state/mobilePagerHeader';
|
||||||
import { useOpenCreateRoomModal } from '../../../state/hooks/createRoomModal';
|
import { useOpenCreateRoomModal } from '../../../state/hooks/createRoomModal';
|
||||||
import { useOpenCreateSpaceModal } from '../../../state/hooks/createSpaceModal';
|
import { useOpenCreateSpaceModal } from '../../../state/hooks/createSpaceModal';
|
||||||
|
import { activeChannelsSpaceAtom } from '../../../state/activeChannelsSpace';
|
||||||
import { CreateRoomType } from '../../../components/create-room/types';
|
import { CreateRoomType } from '../../../components/create-room/types';
|
||||||
import { ChannelsList } from './ChannelsList';
|
import { ChannelsList } from './ChannelsList';
|
||||||
import { ChannelsLanding } from './ChannelsLanding';
|
import { ChannelsLanding } from './ChannelsLanding';
|
||||||
import { ChannelsWorkspaceHorseshoe } from './ChannelsWorkspaceHorseshoe';
|
import { ChannelsWorkspaceHorseshoe } from './ChannelsWorkspaceHorseshoe';
|
||||||
import { WorkspaceFooter } from './WorkspaceFooter';
|
import { WorkspaceFooter } from './WorkspaceFooter';
|
||||||
import { ACTIVE_SPACE_KEY } from './useActiveSpace';
|
|
||||||
|
|
||||||
// Index route at /channels/ (no space selected). Renders the shared
|
// Index route at /channels/ (no space selected). Renders the shared
|
||||||
// StreamHeader (segment switcher) plus the resolve-active-space-or-
|
// StreamHeader (segment switcher) plus the resolve-active-space-or-
|
||||||
|
|
@ -79,18 +80,17 @@ export function Channels() {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const inPagerMode = useMobilePagerPane() !== null;
|
const inPagerMode = useMobilePagerPane() !== null;
|
||||||
const openCreateRoomModal = useOpenCreateRoomModal();
|
const openCreateRoomModal = useOpenCreateRoomModal();
|
||||||
|
const setActiveSpace = useSetAtom(activeChannelsSpaceAtom);
|
||||||
|
|
||||||
// Persist URL-driven active space so cold-starts at /channels/ resume on
|
// Persist URL-driven active space so cold-starts at /channels/ resume on
|
||||||
// the same workspace. `useActiveSpace` (in ChannelsLanding) reads the
|
// the same workspace. `useActiveSpace` (in ChannelsLanding and
|
||||||
// value but never writes it because the index route has no
|
// MobileTabsPager) subscribes to the same atom, so this write
|
||||||
// :spaceIdOrAlias param — the write happens here.
|
// immediately invalidates their cached reads — without that, the pager
|
||||||
|
// would serve a stale `destinationFor('channels')` after a switcher
|
||||||
|
// pick + tab swap on native.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
setActiveSpace(space.roomId);
|
||||||
localStorage.setItem(ACTIVE_SPACE_KEY, space.roomId);
|
}, [setActiveSpace, space.roomId]);
|
||||||
} catch {
|
|
||||||
/* private mode / quota — non-fatal */
|
|
||||||
}
|
|
||||||
}, [space.roomId]);
|
|
||||||
|
|
||||||
// Plus on the tabs row creates a channel inside the active workspace.
|
// Plus on the tabs row creates a channel inside the active workspace.
|
||||||
// Single entry point for «create» on the Channels tab.
|
// Single entry point for «create» on the Channels tab.
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,10 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
import { activeChannelsSpaceAtom } from '../../../state/activeChannelsSpace';
|
||||||
import { getCanonicalAliasRoomId, isRoomAlias } from '../../../utils/matrix';
|
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 => {
|
const safeDecode = (raw: string): string | undefined => {
|
||||||
try {
|
try {
|
||||||
return decodeURIComponent(raw);
|
return decodeURIComponent(raw);
|
||||||
|
|
@ -23,16 +15,22 @@ const safeDecode = (raw: string): string | undefined => {
|
||||||
|
|
||||||
// Resolves the active Space for the channels segment. Priority:
|
// Resolves the active Space for the channels segment. Priority:
|
||||||
// 1. URL `:spaceIdOrAlias` param (if joined-orphan)
|
// 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
|
// 3. first joined-orphan Space
|
||||||
// Returns undefined when the user has 0 joined orphan spaces. Persistence
|
// Returns undefined when the user has 0 joined orphan spaces. Persistence
|
||||||
// (writing to localStorage) lives in `Channels.tsx`, where the inner
|
// (writing the atom) lives in `Channels.tsx`, where the inner route
|
||||||
// route already has the resolved space context — at the index `/channels/`
|
// already has the resolved space context — at the index `/channels/`
|
||||||
// route, useParams().spaceIdOrAlias is always undefined.
|
// 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 => {
|
export const useActiveSpace = (orphanSpaceIds: string[]): string | undefined => {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const { spaceIdOrAlias } = useParams();
|
const { spaceIdOrAlias } = useParams();
|
||||||
const orphanSet = useMemo(() => new Set(orphanSpaceIds), [orphanSpaceIds]);
|
const orphanSet = useMemo(() => new Set(orphanSpaceIds), [orphanSpaceIds]);
|
||||||
|
const persisted = useAtomValue(activeChannelsSpaceAtom);
|
||||||
|
|
||||||
const urlSpaceId = useMemo(() => {
|
const urlSpaceId = useMemo(() => {
|
||||||
if (!spaceIdOrAlias) return undefined;
|
if (!spaceIdOrAlias) return undefined;
|
||||||
|
|
@ -42,10 +40,10 @@ export const useActiveSpace = (orphanSpaceIds: string[]): string | undefined =>
|
||||||
return resolved && orphanSet.has(resolved) ? resolved : undefined;
|
return resolved && orphanSet.has(resolved) ? resolved : undefined;
|
||||||
}, [mx, spaceIdOrAlias, orphanSet]);
|
}, [mx, spaceIdOrAlias, orphanSet]);
|
||||||
|
|
||||||
const persistedSpaceId = useMemo(() => {
|
const persistedSpaceId = useMemo(
|
||||||
const stored = readPersisted();
|
() => (persisted && orphanSet.has(persisted) ? persisted : undefined),
|
||||||
return stored && orphanSet.has(stored) ? stored : undefined;
|
[persisted, orphanSet]
|
||||||
}, [orphanSet]);
|
);
|
||||||
|
|
||||||
return urlSpaceId ?? persistedSpaceId ?? orphanSpaceIds[0];
|
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