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
42 changes: 36 additions & 6 deletions crates/tinyclient/src/gui/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ pub fn plan(window: f32, sidebar: f32, hidden: bool, right: Option<f32>) -> Plan
Plan { sidebar, right }
}

/// Add the Discord parent column only when both lists and the conversation fit.
fn discord_overview_width(window: f32, sidebar: f32, hidden: bool, right: Option<f32>, server: bool) -> f32 {
const OVERVIEW: f32 = 280.;
if server && !hidden && window >= RAIL_WIDTH + sidebar + OVERVIEW + CONVERSATION_MIN + right.unwrap_or(0.) {
OVERVIEW
} else {
0.
}
}

/// The right pane would replace the conversation here rather than sit beside it: Details
/// hides then, and a thread or search stands in for the conversation (07 §4).
pub fn narrow(window: f32, sidebar: f32, hidden: bool, right: f32) -> bool {
Expand Down Expand Up @@ -337,9 +347,10 @@ impl Shell {
return;
}
let x = f32::from(event.position.x);
let overview = discord_overview_width(f32::from(window.viewport_size().width), self.layout_state.sidebar_width, self.layout_state.sidebar_hidden, self.layout_state.right.as_ref().filter(|_| !self.pane_tucked()).map(|_| self.layout_state.right_width), matches!(&self.sidebar_state.mode, super::sidebar::Mode::Space(space) if space.network == Network::Discord));
let state = &mut self.layout_state;
match edge {
Edge::Sidebar => state.sidebar_width = (x - RAIL_WIDTH).round().clamp(SIDEBAR_RANGE.0, SIDEBAR_RANGE.1),
Edge::Sidebar => state.sidebar_width = (x - RAIL_WIDTH - overview).round().clamp(SIDEBAR_RANGE.0, SIDEBAR_RANGE.1),
Edge::Right => {
let window = f32::from(window.viewport_size().width);
state.right_width = (window - x).round().clamp(RIGHT_RANGE.0, RIGHT_RANGE.1);
Expand Down Expand Up @@ -581,12 +592,14 @@ impl Shell {
}

/// The open menu, over everything, with a catcher behind it that closes it.
fn menu_layer(&self, snapshot: &Snapshot, sidebar: f32, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
fn menu_layer(&self, snapshot: &Snapshot, sidebar: f32, overview: f32, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
let open = self.layout_state.menu.as_ref()?;
let tall = f32::from(window.viewport_size().height);
// The rail's add button and own avatar, and the sidebar strip's trailing button.
let rail_button = |from_bottom: f32| place::anchor(16., tall - from_bottom - 40., 40., 40.);
let strip_button = place::anchor(RAIL_WIDTH + sidebar - 12. - 28., 12., 28., 28.);
let strip_left = RAIL_WIDTH + if matches!(open, Menu::Space(_)) { overview } else { 0. };
let strip_width = if overview > 0. && matches!(open, Menu::Network(_)) { overview } else { sidebar };
let strip_button = place::anchor(strip_left + strip_width - 12. - 28., 12., 28., 28.);
let placed: AnyElement = match open {
Menu::Add => {
let content = fade_in("menu-add", 120, self.add_menu(snapshot, cx));
Expand Down Expand Up @@ -825,8 +838,9 @@ impl Render for Shell {
// Too narrow for the pane beside the conversation: sticky Details hides (07 §4).
self.note_room(narrow(width, state.sidebar_width, state.sidebar_hidden, state.right_width), None);
let right_width = state.right.as_ref().filter(|_| !self.pane_tucked()).map(|_| state.right_width);
let plan = plan(width, state.sidebar_width, state.sidebar_hidden, right_width);
self.note_room(self.pane_narrow(), Some(conversation_width(width, &plan)));
let overview = discord_overview_width(width, state.sidebar_width, state.sidebar_hidden, right_width, matches!(&self.sidebar_state.mode, super::sidebar::Mode::Space(space) if space.network == Network::Discord));
let plan = plan(width - overview, state.sidebar_width, state.sidebar_hidden, right_width);
self.note_room(self.pane_narrow(), Some(conversation_width(width - overview, &plan)));
let floating = plan.sidebar.is_none() && state.floating;
let replaced = plan.right.is_some_and(|(_, replaces)| replaces);
// With nothing open in a window too narrow for the sidebar, the list is the main view:
Expand All @@ -835,6 +849,7 @@ impl Render for Shell {
let _ = tall;

root.child(self.rail(snapshot, window, cx))
.when(overview > 0., |root| root.child(self.discord_overview(snapshot, overview, window, cx)))
.children(plan.sidebar.map(|width| {
self.sidebar(snapshot, width, window, cx).child(self.resize_handle(Edge::Sidebar, cx))
}))
Expand Down Expand Up @@ -874,7 +889,7 @@ impl Render for Shell {
)
})
.children(self.switcher(window, cx))
.children(self.menu_layer(snapshot, plan.sidebar.unwrap_or(state.sidebar_width), window, cx))
.children(self.menu_layer(snapshot, plan.sidebar.unwrap_or(state.sidebar_width), overview, window, cx))
.children(self.toast_layer(width, cx))
}
}
Expand Down Expand Up @@ -911,6 +926,21 @@ impl Shell {
mod tests {
use super::*;

#[test]
fn discord_columns_keep_a_readable_conversation_and_yield_to_narrow_windows() {
let minimum = RAIL_WIDTH + SIDEBAR_WIDTH + 280. + CONVERSATION_MIN;
assert_eq!(discord_overview_width(minimum, SIDEBAR_WIDTH, false, None, true), 280.);
assert_eq!(discord_overview_width(minimum - 1., SIDEBAR_WIDTH, false, None, true), 0.);
assert_eq!(discord_overview_width(1800., SIDEBAR_WIDTH, true, None, true), 0.);
assert_eq!(discord_overview_width(1800., SIDEBAR_WIDTH, false, None, false), 0.);
assert_eq!(discord_overview_width(minimum, SIDEBAR_WIDTH, false, Some(RIGHT_WIDTH), true), 0.);
let window = minimum + RIGHT_WIDTH;
let overview = discord_overview_width(window, SIDEBAR_WIDTH, false, Some(RIGHT_WIDTH), true);
let planned = plan(window - overview, SIDEBAR_WIDTH, false, Some(RIGHT_WIDTH));
assert_eq!(planned.right, Some((RIGHT_WIDTH, false)));
assert_eq!(conversation_width(window - overview, &planned), CONVERSATION_MIN);
}

#[test]
fn the_conversation_gives_up_width_then_the_right_pane_replaces_it_then_the_sidebar_leaves() {
// Room for everything.
Expand Down
43 changes: 36 additions & 7 deletions crates/tinyclient/src/gui/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,9 @@ impl Shell {
}
let state = &self.sidebar_state;
state.inbox_list.scroll_to(gpui::ListOffset::default());
state.home_list.scroll_to(gpui::ListOffset::default());
if !matches!(&state.mode, Mode::Space(space) if space.network == Network::Discord) {
state.home_list.scroll_to(gpui::ListOffset::default());
}
state.tree_scroll.scroll_to_item(0, ScrollStrategy::Top);
cx.notify();
return;
Expand All @@ -320,13 +322,20 @@ impl Shell {
state.inbox.fill(epoch, fresh, total);
}
}
let home_network = mode.network().filter(|_| matches!(mode, Mode::Network(_)));
let keep_discord = state.mode.network() == Some(Network::Discord) && mode.network() == Some(Network::Discord);
let home_network = match &mode {
Mode::Space(space) if space.network == Network::Discord => Some(Network::Discord),
Mode::Network(network) => Some(*network),
_ => None,
};
state.home.switch_network(home_network, &mut state.saved_homes);
state.mode = mode;
state.cursor = None;
state.drill = None;
state.slack_menu = false;
state.home_unread = false;
if !keep_discord {
state.home_unread = false;
}
state.apps_open = false;
state.apps_all = false;
state.pill = 0;
Expand Down Expand Up @@ -680,6 +689,9 @@ impl Shell {
/// Opens a row the user chose by hand: the unread walk starts over from there.
fn pick_row(&mut self, row: &Conversation, cx: &mut Context<Self>) {
self.sidebar_state.walked.clear();
if row.key.network == Network::Discord && row.kind.is_direct() && matches!(&self.sidebar_state.mode, Mode::Space(space) if space.network == Network::Discord) {
self.go(Mode::Network(Network::Discord), cx);
}
if row.kind == ConversationKind::Forum && row.key.network == Network::Telegram {
self.drill(home::Drill::Topics(Rc::new(row.clone())), cx);
return;
Expand Down Expand Up @@ -842,6 +854,10 @@ impl Shell {
/// The people whose presence these rows draw, and the rows themselves for their previews.
fn watch_rows<'a>(&self, rows: impl Iterator<Item = &'a Rc<Conversation>> + Clone, cx: &mut Context<Self>) {
let hub = self.people_state.hub();
let home = &self.sidebar_state.home_items;
let visible = on_screen(&self.sidebar_state.home_list, home.len(), |ix| home[ix].height());
let extra = home[visible].iter().filter_map(home::Item::conversation).filter(|_| matches!(&self.sidebar_state.mode, Mode::Space(space) if space.network == Network::Discord));
let rows = rows.cloned().chain(extra.cloned());
let peers = rows.clone().filter_map(|c| Some(Key { id: c.peer.clone()?, ..c.key.clone() }));
hub.watch(WATCH, peers, cx);
hub.watch_rows(WATCH, rows.map(|c| c.key.clone()), cx);
Expand Down Expand Up @@ -1048,7 +1064,7 @@ impl Shell {
out
}

fn title_strip(&self, cx: &mut Context<Self>) -> Div {
fn title_strip(&self, mode: &Mode, cx: &mut Context<Self>) -> Div {
let strip = drags_window(div()).h(px(theme::STRIP)).flex_none().pl(px(theme::PANE_PAD)).pr(px(12.)).flex().items_center().gap(px(8.));
let quiet = |button: Stateful<Div>| button.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation());
let back = |id: &'static str, words: String, cx: &mut Context<Self>| {
Expand All @@ -1073,7 +1089,7 @@ impl Shell {
.child(pane_title(title))
.children(place.map(|place| div().flex_none().max_w(px(120.)).truncate().type_small().text_color(theme::text_faint()).child(place)));
}
match &self.sidebar_state.mode {
match mode {
Mode::Inbox => {
let filter = self.sidebar_state.filter;
let counts = self.sidebar_state.counts;
Expand Down Expand Up @@ -1333,9 +1349,22 @@ impl Shell {
}
}

/// The persistent Discord parent column, using the same home list and scroll state.
pub(super) fn discord_overview(&self, snapshot: &Snapshot, width: f32, window: &mut Window, cx: &mut Context<Self>) -> Div {
self.sidebar_state.width.set(width);
div().w(px(width)).h_full().flex_none().bg(theme::sidebar()).border_r_1().border_color(theme::hairline()).child(
div().id("discord-overview").size_full().flex().flex_col()
.child(self.title_strip(&Mode::Network(Network::Discord), cx))
.child(self.jump_strip(cx))
.child(div().flex_1().min_h_0().child(self.home_list(snapshot, window, cx)))
)
}

pub(super) fn sidebar(&self, snapshot: &Snapshot, width: f32, window: &mut Window, cx: &mut Context<Self>) -> Div {
let people = self.sidebar_state.mode == Mode::People;
self.sidebar_state.width.set(width);
if !matches!(&self.sidebar_state.mode, Mode::Space(space) if space.network == Network::Discord) {
self.sidebar_state.width.set(width);
}
let body = if people {
let drawn = self.slots.people.and_then(|draw| draw(self, window, cx));
drawn.unwrap_or_else(|| empty_state("icons/users.svg", "People", "Friends and contacts show up here.").px(px(24.)).into_any_element())
Expand All @@ -1354,7 +1383,7 @@ impl Shell {
.bg(theme::sidebar())
.border_r_1()
.border_color(theme::hairline())
.child(self.title_strip(cx))
.child(self.title_strip(&self.sidebar_state.mode, cx))
.when(!people, |sidebar| sidebar.child(self.jump_strip(cx)))
.children(tabs)
.when(!banners.is_empty(), |sidebar| sidebar.child(div().flex_none().px(px(theme::INSET)).pb(px(8.)).flex().flex_col().gap(px(4.)).children(banners)))
Expand Down
26 changes: 18 additions & 8 deletions crates/tinyclient/src/gui/sidebar/home_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ fn whole(network: Network, folder: Option<String>, shelf: Shelf) -> std::io::Res
impl Shell {
/// Reads what the home on show draws. Asking is also what makes chatd bring it up to date.
pub(in crate::gui) fn load_home(&mut self, cx: &mut Context<Self>) {
let Mode::Network(network) = self.sidebar_state.mode else {
return;
let network = match &self.sidebar_state.mode {
Mode::Network(network) => *network,
Mode::Space(space) if space.network == Network::Discord => Network::Discord,
_ => return,
};
// Keys only: the read runs off this thread.
let drill = self.sidebar_state.drill.as_ref().map(|drill| match drill {
Expand Down Expand Up @@ -156,9 +158,12 @@ impl Shell {
let state = &self.sidebar_state;
let items: Vec<Item> = match (&state.mode, &state.drill) {
(Mode::Network(_), Some(_)) => state.home.drilled.iter().cloned().map(Item::Chat).collect(),
(Mode::Network(Network::Discord), None) => {
(Mode::Network(Network::Discord), None) | (Mode::Space(Key { network: Network::Discord, .. }), None) => {
let spaces: Vec<protocol::Space> = self.spaces.iter().filter(|s| s.key.network == Network::Discord).cloned().collect();
let open_space = self.snapshot.as_ref().and_then(|s| s.open.as_ref()).and_then(|open| open.space.clone());
let open_space = match &state.mode {
Mode::Space(space) => Some(space.id.clone()),
_ => self.snapshot.as_ref().and_then(|s| s.open.as_ref()).and_then(|open| open.space.clone()),
};
let items = home::discord(&state.home, &spaces, open_space.as_deref(), state.home_unread);
// The servers' indexes are into the shell's whole list of Spaces.
items
Expand All @@ -182,7 +187,7 @@ impl Shell {
}
_ => Vec::new(),
};
let news = if matches!(self.sidebar_state.mode, Mode::Network(Network::Discord)) && self.sidebar_state.drill.is_none() {
let news = if self.sidebar_state.mode.network() == Some(Network::Discord) && self.sidebar_state.drill.is_none() {
let state = &self.sidebar_state;
let spaces: Vec<protocol::Space> = self.spaces.iter().filter(|s| s.key.network == Network::Discord).cloned().collect();
home::discord(&state.home, &spaces, None, true).iter().filter(|item| matches!(item, Item::Chat(_) | Item::Space { .. } | Item::Folder { .. })).count() as u32
Expand Down Expand Up @@ -212,7 +217,12 @@ impl Shell {
self.people_scope(Some(Network::Discord), cx);
return;
}
Act::Requests => return self.drill(Drill::Requests, cx),
Act::Requests => {
if matches!(self.sidebar_state.mode, Mode::Space(_)) {
self.go(Mode::Network(Network::Discord), cx);
}
return self.drill(Drill::Requests, cx);
},
Act::Archive => return self.drill(Drill::Archive, cx),
Act::MoreDms => home.dms_all = true,
Act::MoreVoice => home.voice_all = true,
Expand Down Expand Up @@ -249,7 +259,7 @@ impl Shell {
let Some(item) = self.sidebar_state.home_items.get(ix).cloned() else {
return div().into_any_element();
};
let focused = self.sidebar_state.cursor == Some(ix) && (self.sidebar_state.focus.is_focused(window) || self.focus.is_focused(window));
let focused = self.sidebar_state.listed() && self.sidebar_state.cursor == Some(ix) && (self.sidebar_state.focus.is_focused(window) || self.focus.is_focused(window));
let network = self.sidebar_state.home.network;
let click = move |row: Stateful<Div>, cx: &mut Context<Self>| row.on_mouse_down(MouseButton::Left, cx.listener(move |this, _, _, cx| this.activate_home(ix, cx)));
match item {
Expand Down Expand Up @@ -306,7 +316,7 @@ impl Shell {
return list_row(("space-gone", ix), theme::row_height(theme::ROW_ONE), false, false, div());
};
let lit = space.attention > 0 || space.unread;
let here = self.open.as_ref().is_some_and(|open| self.snapshot.as_ref().and_then(|s| s.open.as_ref()).is_some_and(|row| row.key == *open && row.space.as_deref() == Some(space.key.id.as_str())));
let here = if let Mode::Space(selected) = &self.sidebar_state.mode { selected == &space.key } else { self.open.as_ref().is_some_and(|open| self.snapshot.as_ref().and_then(|s| s.open.as_ref()).is_some_and(|row| row.key == *open && row.space.as_deref() == Some(space.key.id.as_str()))) };
let icon = self.media_path(&space.key, space.icon.as_ref());
let color = if space.muted { theme::text_faint() } else if lit { theme::text() } else { theme::text_soft() };
// A team's chevron sits in the tree's 16 slot, so its name lines up with its channels'.
Expand Down
2 changes: 1 addition & 1 deletion crates/tinyclient/src/gui/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ impl Shell {
let mut directs = false;
for key in dirty.rows {
let in_tree = self.trees.values().any(|tree| tree.conversations.iter().any(|c| c.key == key));
directs |= !in_tree && matches!(self.sidebar_state.mode, Mode::Network(network) if network == key.network);
directs |= !in_tree && self.sidebar_state.home.network == Some(key.network);
if in_tree || self.open.as_ref() == Some(&key) {
self.load_row(key, cx);
}
Expand Down
18 changes: 18 additions & 0 deletions docs/design/polish/02-discord-slack.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,23 @@ For reconciliation with `polish/01-system.md`: `LEAD 20` (leading box of every 2
- **Hover swaps** happen inside a trailing slot of fixed width so the name never re-truncates. Presence and the group's front face take `theme::over(fill, sidebar)` as their ring per state (#121315, #202123, #2E2F31).
- The private-channel lock and the spine are child divs; no masks, no blur. Typing dots are the only timer, in their own entity.


### Discord server columns

At wide window sizes, selecting a Discord server keeps the Discord overview
visible in a 280 pt column and opens its channels in the regular sidebar beside
it. The overview retains DMs, folders, expansion state and scroll position;
selecting another server replaces only the channels column and highlights that
server immediately. Choosing a DM returns to the Discord home and hides the
channels column. Message requests likewise return to the home before drilling
into their list. Home data continues refreshing while a server is selected.

The extra column appears only when the rail, both lists, the 392 pt minimum
conversation and any visible right pane fit. Otherwise the original single
sidebar and back navigation apply. The existing sidebar resize handle changes
the channels width; the overview width is fixed. Other networks keep their
existing layout.


## Deliberately absent
A server-icon column; chevrons and inline trees on server rows; banners, boosts, member and online counts; role colours; "@" in mention badges; a grey muted-mention look; mute bells on rows; elapsed timers and rich-presence art; time on Discord DM rows at rest; a read-only mark; unread-first sorting; prefix grouping; Slack's Unreads, Activity, Later, Huddles and Canvases views (Open in Slack); total reply count in a Threads row (in the pane header); any animation of rows filling in.
Loading