Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions crates/tinyclient/src/gui/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ pub struct State {
opened: HashSet<String>,
closed: HashSet<String>,
quiet_shown: HashSet<String>,
threads_shown: HashSet<Key>,
muted_shown: HashSet<Key>,
opted_shown: HashSet<Key>,
/// The tree on show, flat. Rebuilt on collapse and when its data changes.
Expand Down Expand Up @@ -176,6 +177,7 @@ impl State {
opened: HashSet::new(),
closed: HashSet::new(),
quiet_shown: HashSet::new(),
threads_shown: HashSet::new(),
muted_shown: HashSet::new(),
opted_shown: HashSet::new(),
rows: Rc::new(Vec::new()),
Expand Down Expand Up @@ -438,6 +440,7 @@ impl Shell {
opened: Some(&state.opened),
closed: Some(&state.closed),
quiet_shown: Some(&state.quiet_shown),
threads_shown: Some(&state.threads_shown),
unread: state.home_unread && space.network == Network::Slack,
};
tree::flatten(space, &tree.groups, &tree.conversations, self.open.as_ref(), depth, &rules)
Expand Down Expand Up @@ -547,6 +550,11 @@ impl Shell {
(tree::Reveal::OptedOut, Some(space)) => {
state.opted_shown.insert(space);
}
(tree::Reveal::Threads(channel), _) => {
if !state.threads_shown.remove(&channel) {
state.threads_shown.insert(channel);
}
}
(tree::Reveal::Quiet(group), _) => {
state.quiet_shown.insert(group);
}
Expand Down Expand Up @@ -868,15 +876,13 @@ impl Shell {
self.action_row(("threads", ix), Some("icons/messages-square.svg"), "Threads".into(), *count > 0, trailing, focused)
.on_mouse_down(MouseButton::Left, cx.listener(move |this, _, _, cx| this.activate(ix, cx)))
}
Row::Fold { words, depth, reveal } => {
let quiet = *reveal == tree::Reveal::Threads;
Row::Fold { words, depth, .. } => {
let content = div()
.children(indent(*depth, 8.))
.gap(px(8.))
.child(div().size(px(20.)).flex_none().flex().items_center().justify_center().child(glyph("icons/ellipsis.svg", 14., theme::text_faint())))
.child(div().flex_1().min_w_0().truncate().type_ui().text_color(theme::text_faint()).child(words.clone()));
list_row(("fold", ix), height, false, focused, content)
.when(quiet, |row| row.cursor_default())
.on_mouse_down(MouseButton::Left, cx.listener(move |this, _, _, cx| this.activate(ix, cx)))
}
Row::More { words, depth } => {
Expand Down
56 changes: 49 additions & 7 deletions crates/tinyclient/src/gui/sidebar/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ pub enum Reveal {
OptedOut,
/// "12 quiet channels" at the end of a Slack section.
Quiet(String),
/// "4 more threads" under a channel: opens nothing yet, it says how many.
Threads,
/// Toggle followed threads under this channel.
Threads(Key),
}

#[derive(Clone, PartialEq, Debug)]
Expand Down Expand Up @@ -96,6 +96,8 @@ pub struct Rules<'a> {
pub closed: Option<&'a HashSet<String>>,
/// Slack sections whose quiet channels are shown.
pub quiet_shown: Option<&'a HashSet<String>>,
/// Channels whose followed threads are expanded this session.
pub threads_shown: Option<&'a HashSet<Key>>,
/// The home's Unread segment: only rows with news, the open one and starred ones.
pub unread: bool,
}
Expand Down Expand Up @@ -130,7 +132,8 @@ pub fn flatten(space: &Key, groups: &[Group], conversations: &[Rc<Conversation>]
let mut quiet_threads: HashMap<&str, usize> = HashMap::new();
for thread in conversations.iter().filter(|c| is_thread(c) && c.parent.as_ref().is_some_and(in_tree)) {
let parent = thread.parent.as_deref().unwrap_or_default();
let shown = open == Some(&thread.key) || (!slack && thread.followed && thread.badge() != Badge::None);
let expanded = rules.threads_shown.is_some_and(|shown| shown.contains(&Key { id: parent.into(), ..space.clone() }));
let shown = open == Some(&thread.key) || (!slack && thread.followed && (expanded || thread.badge() != Badge::None));
if shown {
threads.entry(parent).or_default().push(thread);
} else if !slack && thread.followed {
Expand Down Expand Up @@ -164,15 +167,16 @@ pub fn flatten(space: &Key, groups: &[Group], conversations: &[Rc<Conversation>]
}
}
rows.push(Row::Conversation { conversation: Rc::clone(conversation), depth });
let expanded = rules.threads_shown.is_some_and(|shown| shown.contains(&conversation.key));
for (ix, thread) in under.iter().enumerate() {
if ix < THREADS_SHOWN || open == Some(&thread.key) {
if expanded || ix < THREADS_SHOWN || open == Some(&thread.key) {
rows.push(Row::Conversation { conversation: Rc::clone(thread), depth: depth + 1 });
}
}
let more = under.iter().enumerate().filter(|(ix, thread)| *ix >= THREADS_SHOWN && open != Some(&thread.key)).count() + quiet_threads.get(conversation.key.id.as_str()).copied().unwrap_or(0);
if more > 0 && !collapsed {
let words = if more == 1 { "1 more thread".to_owned() } else { format!("{more} more threads") };
rows.push(Row::Fold { words, reveal: Reveal::Threads, depth: depth + 1 });
if !slack && !collapsed && (more > 0 || expanded) {
let words = if expanded { "Show fewer threads".to_owned() } else if more == 1 { "1 more thread".to_owned() } else { format!("{more} more threads") };
rows.push(Row::Fold { words, reveal: Reveal::Threads(conversation.key.clone()), depth: depth + 1 });
}
if let Some(inside) = conversation.occupants.as_ref().filter(|_| occupied(conversation)) {
rows.extend(inside.people.iter().take(OCCUPANTS_SHOWN).map(|occupant| Row::Occupant { occupant: occupant.clone(), depth: depth + 1 }));
Expand Down Expand Up @@ -398,6 +402,44 @@ mod tests {
assert_eq!(rows(&[], all, Some("t5")), ["general", ".t0", ".t1", ".t2", ".t5", "2 more threads"]);
}

#[test]
fn expanding_threads_reveals_read_and_overflow_threads_only_for_that_channel() {
use ConversationKind::{Channel, Thread};
let mut all = vec![channel("general", None, Channel), channel("other", None, Channel)];
for n in 0..7 {
let mut thread = channel(&format!("t{n}"), None, Thread);
thread.parent = Some(if n == 6 { "other" } else { "general" }.into());
thread.followed = n != 5;
thread.unread = u32::from(n < 4);
all.push(thread);
}
let shown = HashSet::from([key("general")]);
let rules = Rules { threads_shown: Some(&shown), ..Rules::default() };
assert_eq!(rows_with(&[], all.clone(), None, &rules), ["general", ".t0", ".t1", ".t2", ".t3", ".t4", "Show fewer threads", "other", "1 more thread"]);
// Collapsing keeps an open overflow thread visible, and hides read threads again.
assert_eq!(rows(&[], all.clone(), Some("t3")), ["general", ".t0", ".t1", ".t2", ".t3", "1 more thread", "other", "1 more thread"]);
let foreign = HashSet::from([Key { account: "another-account".into(), ..key("general") }]);
let rules = Rules { threads_shown: Some(&foreign), ..Rules::default() };
assert_eq!(rows_with(&[], all, None, &rules), ["general", ".t0", ".t1", ".t2", "2 more threads", "other", "1 more thread"]);
}

#[test]
fn one_read_thread_can_be_revealed_and_its_fold_targets_the_parent() {
use ConversationKind::{Channel, Thread};
let mut thread = channel("read", None, Thread);
thread.parent = Some("general".into());
thread.followed = true;
let all = vec![Rc::new(channel("general", None, Channel)), Rc::new(thread)];
let collapsed = flatten(&key("s"), &[], &all, None, 0, &Rules::default());
assert_eq!(shape(&collapsed), ["general", "1 more thread"]);
assert!(matches!(&collapsed[1], Row::Fold { reveal: Reveal::Threads(parent), .. } if parent == &key("general")));
let shown = HashSet::from([key("general")]);
let rules = Rules { threads_shown: Some(&shown), ..Rules::default() };
let expanded = flatten(&key("s"), &[], &all, None, 0, &rules);
assert_eq!(shape(&expanded), ["general", ".read", "Show fewer threads"]);
assert!(matches!(&expanded[2], Row::Fold { reveal: Reveal::Threads(parent), .. } if parent == &key("general")));
}

#[test]
fn hide_muted_and_opt_in_hide_rows_behind_footers_that_reveal_them() {
use ConversationKind::Channel;
Expand Down
2 changes: 1 addition & 1 deletion docs/design/polish/02-discord-slack.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Top to bottom, one scroll:
- **At most one row under it**: a live event or one within 24 h (calendar glyph, title, time `text_faint` right). P2.
- **Order** is Discord's (`position`, text before voice). Category headers are section headers. No unread-first sort.
- **Kinds, by glyph only**: `#` text · megaphone announcement · speaker voice · radio stage · two-bubble forum · image media. **Private**: the kind glyph with a lock 12 at its lower right on a 1 pt plate of the row colour. **Read-only**: no row mark; the conversation replaces the composer with a line. **Muted**: faint, no glyph. **NSFW**: unchanged row (the gate is in the conversation).
- **Followed threads** (only joined and unread or open, at most 3): corner glyph 14, name, count badge; then "n more threads" action sub-row opening the list in the right pane. A thread whose starter is gone uses its own name.
- **Followed threads** (only joined and unread or open, at most 3): corner glyph 14, name, count badge; then "n more threads" action sub-row expanding all followed threads under that channel, including read ones. "Show fewer threads" collapses back to the three-row limit; the open thread stays visible. Expansion is remembered per channel for the session. A thread whose starter is gone uses its own name.
- **Voice**: occupied channel `text_soft` with `4/12` when it has a user limit; occupants as 28 sub-rows (avatar 20, server nickname, trailing mic-off / deafen 14 `danger`, screen share or camera 14 `text_soft`), 6 then "+n", friends first. Empty voice channel: `text_faint`, nothing trailing. Stage: "Live · 2 speaking" `text_faint`.
- **Footers** (28 action rows, `text_faint`): "Show 41 muted channels" when *Hide muted channels* is on in Discord (reveals for the session); "Browse 212 more channels" for onboarding opt-outs (from the stored tree).
- **Collapse**: muted categories start collapsed; over 80 visible channels, every category without a number starts collapsed. Collapsed still shows the open row, unread unmuted channels, rows with a number and voice channels with people. The user's own collapse wins after and is kept per server.
Expand Down
Loading