fix(mobile): Spaces tab remembers last-selected space across DM/Friends/Settings detours

Adds sticky `lastSelectedSpaceId` to useSpaceStore. Updated on every
setCurrentSpace(non-null) and on loadSpaceDetail success; NOT cleared by
setCurrentSpace(null), so the @me URL effect in AppLayout no longer wipes
the memory. Cleared only when the remembered space is actually removed
(deleteSpace / leaveSpace / removeSpace / removeInstanceSpaces / reset).
Ephemeral — URL drives initial state on reload.

MobileBottomNav.handleTab('spaces') and MobileSpacesScreen's initial
useState now resolve via `currentSpaceId ?? lastSelectedSpaceId`. The
prior Object.keys(lastChannelPerSpace)[0] fallback was wrong twice over:
first-inserted ordering locked the tab to whichever space the user
opened first in their session, and any approach reading currentSpaceId
alone breaks after a /channels/@me detour.

Also fixes a pre-existing render loop in MobileSpacesScreen's
currentSpaceId sync effect — `selectedSpaceId` removed from deps,
functional setState makes the update idempotent.

Spec: docs/systems/mobile-ui.md updates Tab Tap Behavior, MobileScreenHeader,
and LocalStorage Persistence sections.
This commit is contained in:
Jannis Braun
2026-05-07 23:00:01 +02:00
parent f733e39b4a
commit d9c9f18b20
4 changed files with 80 additions and 12 deletions
+33 -1
View File
@@ -59,6 +59,19 @@ export interface UserViewEntry {
interface SpaceState {
spaces: TaggedSpace[];
currentSpaceId: string | null;
/**
* Sticky memory of the most-recently-selected space. Updated on every
* `setCurrentSpace(non-null)` and when `loadSpaceDetail` lands. Crucially, it
* is NOT cleared by `setCurrentSpace(null)` (the @me / DMs navigation case)
* so mobile callers can answer "which space should the Spaces tab return to
* after a side trip through DMs?" — `currentSpaceId` is wiped on @me by
* AppLayout's URL effect, which would otherwise force a fallback to
* `spaces[0]`. Cleared only when the remembered space is actually removed
* (deleteSpace / leaveSpace / removeSpaceFromState / removeInstanceSpaces /
* reset). Ephemeral — not persisted, since URL drives initial state on
* reload.
*/
lastSelectedSpaceId: string | null;
channels: Channel[];
categories: ChannelCategory[];
members: MemberWithUser[];
@@ -172,6 +185,7 @@ async function pushLayoutToOrigin(
export const useSpaceStore = create<SpaceState>((set, get) => ({
spaces: [],
currentSpaceId: null,
lastSelectedSpaceId: null,
channels: [],
categories: [],
members: [],
@@ -196,6 +210,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
set({
spaces: [],
currentSpaceId: null,
lastSelectedSpaceId: null,
channels: [],
categories: [],
members: [],
@@ -218,7 +233,14 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
},
setSpaces: (spaces) => set({ spaces }),
setCurrentSpace: (spaceId) => set({ currentSpaceId: spaceId }),
setCurrentSpace: (spaceId) =>
set((state) => ({
currentSpaceId: spaceId,
// Only update sticky memory when actually selecting a space. Clearing
// currentSpaceId (e.g. on @me navigation) must NOT wipe the memory —
// that's the whole point of this slot.
lastSelectedSpaceId: spaceId !== null ? spaceId : state.lastSelectedSpaceId,
})),
setChannels: (channels) => set({ channels }),
setCategories: (categories) => set({ categories }),
setMembers: (members) => set({ members }),
@@ -351,6 +373,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
set({
loadingSpaceId: null,
currentSpaceId: spaceId,
lastSelectedSpaceId: spaceId,
channels: detail.channels.sort((a, b) => a.position - b.position),
categories: (detail.categories || []).sort((a, b) => a.position - b.position),
members: detail.members,
@@ -392,6 +415,8 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
set((state) => ({
spaces: state.spaces.filter(s => s.id !== spaceId),
currentSpaceId: state.currentSpaceId === spaceId ? null : state.currentSpaceId,
lastSelectedSpaceId:
state.lastSelectedSpaceId === spaceId ? null : state.lastSelectedSpaceId,
}));
},
@@ -405,6 +430,8 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
set((state) => ({
spaces: state.spaces.filter(s => s.id !== spaceId),
currentSpaceId: state.currentSpaceId === spaceId ? null : state.currentSpaceId,
lastSelectedSpaceId:
state.lastSelectedSpaceId === spaceId ? null : state.lastSelectedSpaceId,
}));
},
@@ -553,6 +580,8 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
return {
spaces: state.spaces.filter(s => s.id !== spaceId),
currentSpaceId: state.currentSpaceId === spaceId ? null : state.currentSpaceId,
lastSelectedSpaceId:
state.lastSelectedSpaceId === spaceId ? null : state.lastSelectedSpaceId,
channelToSpaceMap,
channelPermissions,
channelOriginMap,
@@ -1022,6 +1051,9 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
currentSpaceId: remainingSpaces.find(s => s.id === state.currentSpaceId)
? state.currentSpaceId
: null,
lastSelectedSpaceId: remainingSpaces.find(s => s.id === state.lastSelectedSpaceId)
? state.lastSelectedSpaceId
: null,
};
});