From 76afb30dfbc385de23e23d840a3e94e9b103badf Mon Sep 17 00:00:00 2001 From: Vincent Bernardoff Date: Sat, 17 Oct 2026 15:15:55 +0200 Subject: [PATCH] fix: restore cached network sidebars immediately Keep network homes and their folds across navigation, then refresh from chatd in the background. Skip the event debounce on explicit navigation and invalidate outstanding reads before saving a home. Restore the root loading state when leaving a sublist so cached empty homes remain ready. Document the session cache and cover navigation, folds, and stale reads with regression tests. All eleven GUI home model tests pass. Co-Authored-By: GPT-6 --- crates/tinyclient/src/gui/sidebar.rs | 9 ++-- crates/tinyclient/src/gui/sidebar/home.rs | 59 ++++++++++++++++++++++- docs/design/polish/02-discord-slack.md | 6 +++ 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/crates/tinyclient/src/gui/sidebar.rs b/crates/tinyclient/src/gui/sidebar.rs index d5e9d09..49376ff 100644 --- a/crates/tinyclient/src/gui/sidebar.rs +++ b/crates/tinyclient/src/gui/sidebar.rs @@ -96,6 +96,7 @@ pub struct State { muted_open: bool, /// The network home on show: what chatd said, and the user's folds. pub home: home::Home, + saved_homes: HashMap, home_list: ListState, home_items: Rc>, /// The home's Unread segment, and how many rows it holds. @@ -165,6 +166,7 @@ impl State { muted_news: Vec::new(), muted_open: false, home: home::Home::default(), + saved_homes: HashMap::new(), home_list: list_state(cx), home_items: Rc::new(Vec::new()), home_unread: false, @@ -319,9 +321,7 @@ impl Shell { } } let home_network = mode.network().filter(|_| matches!(mode, Mode::Network(_))); - if home_network != state.home.network { - state.home = home_network.map(home::Home::for_network).unwrap_or_default(); - } + state.home.switch_network(home_network, &mut state.saved_homes); state.mode = mode; state.cursor = None; state.drill = None; @@ -338,7 +338,8 @@ impl Shell { hub.watch_rows(WATCH, [], cx); self.show_mode(cx); self.rebuild_inbox(cx); - self.load_directs(cx); + // Navigation reads immediately; only background event bursts are debounced. + self.load_home(cx); } fn here(&self) -> Location { diff --git a/crates/tinyclient/src/gui/sidebar/home.rs b/crates/tinyclient/src/gui/sidebar/home.rs index e47b1fc..e64532f 100644 --- a/crates/tinyclient/src/gui/sidebar/home.rs +++ b/crates/tinyclient/src/gui/sidebar/home.rs @@ -5,7 +5,7 @@ //! mixed, so the list is a GPUI `list` whose heights are known from each item's kind. use protocol::{Badge, Conversation, Folder, Key, Network, Space, Special}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::rc::Rc; /// Discord DMs before "Show n more" (product rulings: 10). @@ -118,6 +118,24 @@ pub struct Home { } impl Home { + /// Save the outgoing home and restore the destination without waiting for chatd. + pub fn switch_network(&mut self, network: Option, saved: &mut HashMap) { + if self.network == network { + return; + } + let mut outgoing = std::mem::take(self); + // An answer started before leaving must never overwrite a restored home. + outgoing.epoch += 1; + outgoing.loaded = outgoing.loaded_once; + outgoing.drilled.clear(); + if let Some(previous) = outgoing.network { + saved.insert(previous, outgoing); + } + *self = network + .map(|n| saved.remove(&n).unwrap_or_else(|| Home::for_network(n))) + .unwrap_or_default(); + } + pub fn for_network(network: Network) -> Home { Home { network: Some(network), ..Home::default() } } @@ -367,6 +385,45 @@ mod tests { .collect() } + #[test] + fn switching_networks_restores_chats_and_folds_without_a_read() { + let mut saved = HashMap::new(); + let mut home = Home { + loaded: true, + loaded_once: true, + chats: vec![chat("club", 0)], + chats_all: true, + epoch: 7, + ..Home::for_network(Network::Signal) + }; + home.switch_network(Some(Network::Discord), &mut saved); + assert!(!home.loaded); + home.switch_network(None, &mut saved); + home.switch_network(Some(Network::Signal), &mut saved); + assert!(home.loaded && home.loaded_once && home.chats_all); + assert_eq!(home.chats[0].key.id, "club"); + assert_ne!(home.epoch, 7, "a pre-navigation read must be stale"); + assert_eq!(shape(&signal(&home), &[]), ["club"]); + } + + #[test] + fn leaving_a_sublist_restores_the_completed_home_loading_state() { + let mut saved = HashMap::new(); + let mut home = Home { + loaded_once: true, + loaded: false, + drilled: vec![chat("request", 0)], + ..Home::for_network(Network::Discord) + }; + home.switch_network(Some(Network::Signal), &mut saved); + home.switch_network(Some(Network::Discord), &mut saved); + assert!(home.loaded); + assert!(home.drilled.is_empty()); + let epoch = home.epoch; + home.switch_network(Some(Network::Discord), &mut saved); + assert_eq!(home.epoch, epoch); + } + #[test] fn dms_show_every_one_with_a_number_then_the_newest_up_to_ten() { let mut chats: Vec<_> = (0..14).map(|n| chat(&format!("d{n}"), 0)).collect(); diff --git a/docs/design/polish/02-discord-slack.md b/docs/design/polish/02-discord-slack.md index f121b29..2ef3440 100644 --- a/docs/design/polish/02-discord-slack.md +++ b/docs/design/polish/02-discord-slack.md @@ -76,6 +76,12 @@ normal order. A completed empty DM read still shows servers. Once the home has loaded, background refreshes keep its existing rows visible, including when returning from a sublist. +Network homes are cached for the GUI session: switching back displays saved +chats and folds immediately, then refreshes from chatd in the background. +Explicit navigation skips the event-burst debounce. A network opened for the +first time still waits for its local chat read. Returning from a sublist restores +the completed home's loading state, including a previously empty home. + ## New tokens For reconciliation with `polish/01-system.md`: `LEAD 20` (leading box of every 28 row), `NAME_X 36`, `INDENT_CHILD 12`, `INDENT_SUB 28`; `tag` component (16, r4, 11/500, `text_soft` on `quiet_fill`, which survives here only as the tag fill); `folder_spine` (the folder's own colour at 70%, 3 wide); `sticky_header` (`sidebar` fill, no rule, per C8). **Divergence to reconcile**: C9 puts DMs in homes on 48 rows; Slack DMs stay 28 because they sit in the user's sections beside channels in one `uniform_list` (Slack's own structure, ruling 1). Discord DMs are 48. Glyphs added: mail, phone, radio, image, building-2, calendar, headphones, headphone-off, monitor, eye, list, moon, smile, log-out, external-link, check-check, wifi-off.