feat(ui): back and forward also step through task lists and lists

The open task list and the open list are part of the place in the
browser history; the app's own choice of a first list replaces the
entry. Going back to a task list shows its tasks from memory right away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-02 22:00:43 +01:00
1 parent 83bba8d20b
commit b3767ab387
3 files changed
+42 -5

No files matched your search

@@ -19,6 +19,7 @@ import { CountBadge } from '../../components/CountBadge';
import { ListSwitcher } from '../../components/ListSwitcher'; import { ListSwitcher } from '../../components/ListSwitcher';
import { LivePill } from '../../components/LivePill'; import { LivePill } from '../../components/LivePill';
import { isPhoneWidth } from '../../lib/breakpoints'; import { isPhoneWidth } from '../../lib/breakpoints';
import { reportNav, onNavStep, restoringNavPlace } from '../../lib/navHistory';
import { OriginBadge } from '../../components/OriginBadge'; import { OriginBadge } from '../../components/OriginBadge';
import { VoiceInputButton } from '../../components/VoiceInputButton'; import { VoiceInputButton } from '../../components/VoiceInputButton';
import { FormSheet } from '../../components/FormSheet'; import { FormSheet } from '../../components/FormSheet';
@@ -92,8 +93,24 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
const { setGroups, setPrimaryAction } = useMobileActions(); const { setGroups, setPrimaryAction } = useMobileActions();
const [lists, setLists] = useState<SharedList[]>([]); const [lists, setLists] = useState<SharedList[]>([]);
// The open list is remembered on this device (other tabs of the app, new tab, restart) // The open list is remembered on this device (other tabs of the app, new tab, restart)
const [activeListId, setActiveListId] = useState<string>( const [activeListId, setActiveListId] = useState<string>(() => {
() => initialListId || readViewMemory<{ listId: string }>('lists').listId || '' // Coming back here by the browser's back/forward: the list of that step
const step = restoringNavPlace();
return (step?.tab === 'lists' && step.shopList) || initialListId || readViewMemory<{ listId: string }>('lists').listId || '';
});
// Switching lists is a step of the browser history (lib/navHistory.ts)
const autoSelectedListRef = useRef(false);
useEffect(() => {
const replace = autoSelectedListRef.current;
autoSelectedListRef.current = false;
if (activeListId) reportNav({ shopList: activeListId }, { replace });
}, [activeListId]);
useEffect(
() =>
onNavStep((place) => {
if (place.tab === 'lists' && place.shopList) setActiveListId(place.shopList);
}),
[]
); );
useEffect(() => { useEffect(() => {
if (activeListId) writeViewMemory('lists', { listId: activeListId }); if (activeListId) writeViewMemory('lists', { listId: activeListId });
@@ -167,6 +184,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
if (initialListId && deduplicated.some((l) => l.id === initialListId)) { if (initialListId && deduplicated.some((l) => l.id === initialListId)) {
setActiveListId(initialListId); setActiveListId(initialListId);
} else if (deduplicated.length > 0 && (!activeListId || !deduplicated.some((l) => l.id === activeListId))) { } else if (deduplicated.length > 0 && (!activeListId || !deduplicated.some((l) => l.id === activeListId))) {
autoSelectedListRef.current = true; // chosen by the app, not a step of its own in the history
setActiveListId(deduplicated[0].id); setActiveListId(deduplicated[0].id);
} }
} catch { } catch {
+16 -1
View File
@@ -42,6 +42,7 @@ import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
import { CountBadge } from '../../components/CountBadge'; import { CountBadge } from '../../components/CountBadge';
import { ListSwitcher } from '../../components/ListSwitcher'; import { ListSwitcher } from '../../components/ListSwitcher';
import { isPhoneWidth } from '../../lib/breakpoints'; import { isPhoneWidth } from '../../lib/breakpoints';
import { reportNav, onNavStep, restoringNavPlace } from '../../lib/navHistory';
import { LivePill } from '../../components/LivePill'; import { LivePill } from '../../components/LivePill';
import { FormSheet } from '../../components/FormSheet'; import { FormSheet } from '../../components/FormSheet';
import { EmojiIconButton } from '../../components/EmojiIconButton'; import { EmojiIconButton } from '../../components/EmojiIconButton';
@@ -181,7 +182,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
const [tasks, setTasks] = useState<Thought[]>([]); const [tasks, setTasks] = useState<Thought[]>([]);
const [taskLists, setTaskLists] = useState<TaskList[]>([]); const [taskLists, setTaskLists] = useState<TaskList[]>([]);
const [selectedListId, setSelectedListIdState] = useState<string>( const [selectedListId, setSelectedListIdState] = useState<string>(
() => remembered.listId || 'all' () => {
// Coming back here by the browser's back/forward: the list of that step
const step = restoringNavPlace();
return (step?.tab === 'tasks' && step.taskList) || remembered.listId || 'all';
}
); );
const viewMode: ViewMode = viewModes[selectedListId] || 'list'; const viewMode: ViewMode = viewModes[selectedListId] || 'list';
// Only Kanban is stored; the list view is the default // Only Kanban is stored; the list view is the default
@@ -365,6 +370,16 @@ export const TasksView: React.FC<TasksViewProps> = ({
setSelectedListIdState(id); setSelectedListIdState(id);
}, []); }, []);
// Switching task lists is a step of the browser history (lib/navHistory.ts)
useEffect(() => reportNav({ taskList: selectedListId }), [selectedListId]);
useEffect(
() =>
onNavStep((place) => {
if (place.tab === 'tasks' && place.taskList) setSelectedListId(place.taskList);
}),
[setSelectedListId]
);
const onRefreshStatsRef = useRef(onRefreshStats); const onRefreshStatsRef = useRef(onRefreshStats);
onRefreshStatsRef.current = onRefreshStats; onRefreshStatsRef.current = onRefreshStats;
// The realtime handler is registered once; it must reload with the current list, not the first one // The realtime handler is registered once; it must reload with the current list, not the first one
+6 -2
View File
@@ -1,5 +1,5 @@
// In-app back and forward: where the user is (page, open note, workspace view, zoomed bullet, on phones // In-app back and forward: where the user is (page, open note, workspace view, zoomed bullet, on phones
// list or note) goes into the browser history, so the browser's back button (mouse button, Android back) // list or note, open task list and list) goes into the browser history, so the browser's back button (mouse button, Android back)
// steps back inside the app instead of leaving it. Each part reports its own piece; changes from one // steps back inside the app instead of leaving it. Each part reports its own piece; changes from one
// action (e.g. opening a note from a task switches page and note) become one entry. While the app follows // action (e.g. opening a note from a task switches page and note) become one entry. While the app follows
// a back/forward step, reports change nothing: the step itself already is the entry. // a back/forward step, reports change nothing: the step itself already is the entry.
@@ -10,6 +10,8 @@ export interface NavPlace {
view?: string | null; // workspace view: editor, todos, tags, trash, journal … view?: string | null; // workspace view: editor, todos, tags, trash, journal …
zoom?: string | null; // bullet the open note is zoomed into zoom?: string | null; // bullet the open note is zoomed into
detail?: boolean; // phones: the note (true) or the list of notes (false) detail?: boolean; // phones: the note (true) or the list of notes (false)
taskList?: string | null; // open task list on the tasks page ("all" for Alle Aufgaben)
shopList?: string | null; // open list on the lists page
} }
const RESTORE_MS = 600; // the app needs a moment to follow a step (views mount, notes load) const RESTORE_MS = 600; // the app needs a moment to follow a step (views mount, notes load)
@@ -25,7 +27,9 @@ const same = (a: NavPlace, b: NavPlace) =>
(a.docId ?? null) === (b.docId ?? null) && (a.docId ?? null) === (b.docId ?? null) &&
(a.view ?? null) === (b.view ?? null) && (a.view ?? null) === (b.view ?? null) &&
(a.zoom ?? null) === (b.zoom ?? null) && (a.zoom ?? null) === (b.zoom ?? null) &&
Boolean(a.detail) === Boolean(b.detail); Boolean(a.detail) === Boolean(b.detail) &&
(a.taskList ?? null) === (b.taskList ?? null) &&
(a.shopList ?? null) === (b.shopList ?? null);
function flush() { function flush() {
if (!pending) return; if (!pending) return;