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.