From cb936cdcbd647cd5421b1d43c5652525e71059b6 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Tue, 16 Jun 2026 15:04:18 -0400 Subject: [PATCH] refactor(w7): move recents into its own card on the home screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A growing recents history shouldn't reflow the Connect card's Create/Join controls. Extract the rendering into a `recents_card` free fn (mirroring `friends_panel`'s self-contained styling) and place it in the left column beneath the Connect card — both are "get into a room" — with Friends on the right. The card is omitted entirely (no stray gap) when empty, in both the narrow (stacked) and wide (row) responsive layouts. Screenshot-verified at the default width: Connect + Recent Rooms stacked left, Friends right; the Connect card stays fixed-size as recents grow. Co-Authored-By: Claude Opus 4.8 --- src/app/mod.rs | 170 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 114 insertions(+), 56 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index a1cea20..03c53a4 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1290,7 +1290,6 @@ fn connect_card(state: &AppState) -> Element<'_, AppMessage> { let color_subtext = pal.subtext; let color_blue = pal.blue; let color_lavender = pal.lavender; - let color_maroon = pal.maroon; let c_style = move |bg: Color, b_color: Color, radius: f32| { move |_theme: &Theme| container::Style { @@ -1327,55 +1326,6 @@ fn connect_card(state: &AppState) -> Element<'_, AppMessage> { selection: color_blue, }; - // Recently-joined rooms (W7 P5): a one-click rejoin list. Only shown when - // non-empty so the launch card stays clean on a fresh install. - let recents_group: Element = if state.config.recents.is_empty() { - column![].into() - } else { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); - let mut rows = column![].spacing(6).width(iced::Length::Fill); - for r in &state.config.recents { - let label = { - let n = crate::sanitize::sanitize_name(&r.name); - if n.is_empty() { "Untitled room".to_string() } else { n } - }; - let when = crate::recents::relative_time(now, r.joined_at); - let entry = button( - row![ - text(label).size(14).color(color_text), - horizontal_space(), - text(when).size(11).color(color_subtext), - ] - .align_y(iced::alignment::Vertical::Center), - ) - .on_press(AppMessage::JoinRecent(r.ticket.clone())) - .style(b_style(color_crust, color_surface, color_text, 6.0)) - .padding(8) - .width(iced::Length::Fill); - rows = rows.push( - row![ - entry, - button(text("✕").size(12)) - .on_press(AppMessage::RemoveRecent(r.ticket.clone())) - .style(b_style(color_surface, color_maroon, color_text, 6.0)) - .padding(8), - ] - .spacing(6) - .align_y(iced::alignment::Vertical::Center), - ); - } - column![ - text("Recent rooms").size(14).color(color_subtext), - vertical_space(4.0), - rows, - ] - .width(iced::Length::Fill) - .into() - }; - let logo = text("PEERSPEAK").size(36).color(color_blue); let subtitle = text("NAT-traversing full-mesh voice chat").size(16).color(color_subtext); @@ -1433,8 +1383,6 @@ fn connect_card(state: &AppState) -> Element<'_, AppMessage> { text("— OR —").size(12).color(color_surface).align_x(iced::alignment::Horizontal::Center), vertical_space(16.0), join_group, - vertical_space(16.0), - recents_group, vertical_space(10.0), status ] @@ -1447,6 +1395,102 @@ fn connect_card(state: &AppState) -> Element<'_, AppMessage> { .into() } +/// The "Recent rooms" card (W7 P5): a one-click rejoin list, in its own card so a +/// growing history never reflows the Connect card's Create/Join controls. Returns +/// an empty element when there are no recents, so the home screen can omit it +/// entirely on a fresh install. Mirrors `friends_panel`'s self-contained styling. +fn recents_card(state: &AppState) -> Element<'_, AppMessage> { + if state.config.recents.is_empty() { + return column![].into(); + } + let pal = state.config.theme.palette(); + let color_crust = pal.crust; + let color_mantle = pal.mantle; + let color_surface = pal.surface; + let color_text = pal.text; + let color_subtext = pal.subtext; + let color_maroon = pal.maroon; + + let c_style = move |bg: Color, b_color: Color, radius: f32| { + move |_theme: &Theme| container::Style { + text_color: Some(color_text), + background: Some(Background::Color(bg)), + border: Border { + color: b_color, + width: if b_color == Color::TRANSPARENT { 0.0 } else { 1.0 }, + radius: radius.into(), + }, + ..Default::default() + } + }; + let b_style = move |bg: Color, hover_bg: Color, text_c: Color, radius: f32| { + move |_theme: &Theme, status: button::Status| { + let active_bg = match status { + button::Status::Hovered => hover_bg, + _ => bg, + }; + button::Style { + background: Some(Background::Color(active_bg)), + text_color: text_c, + border: Border { color: Color::TRANSPARENT, width: 0.0, radius: radius.into() }, + ..Default::default() + } + } + }; + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + let mut rows = column![].spacing(6).width(iced::Length::Fill); + for r in &state.config.recents { + let label = { + let n = crate::sanitize::sanitize_name(&r.name); + if n.is_empty() { "Untitled room".to_string() } else { n } + }; + let when = crate::recents::relative_time(now, r.joined_at); + let entry = button( + row![ + text(label).size(14).color(color_text), + horizontal_space(), + text(when).size(11).color(color_subtext), + ] + .align_y(iced::alignment::Vertical::Center), + ) + .on_press(AppMessage::JoinRecent(r.ticket.clone())) + .style(b_style(color_crust, color_surface, color_text, 6.0)) + .padding(8) + .width(iced::Length::Fill); + rows = rows.push( + row![ + entry, + button(text("✕").size(12)) + .on_press(AppMessage::RemoveRecent(r.ticket.clone())) + .style(b_style(color_surface, color_maroon, color_text, 6.0)) + .padding(8), + ] + .spacing(6) + .align_y(iced::alignment::Vertical::Center), + ); + } + + container( + column![ + text("RECENT ROOMS").size(18).color(color_text), + text("Rooms you've been in — click to hop back. Best-effort: only works while someone's still there.") + .size(11) + .color(color_subtext), + vertical_space(10.0), + rows, + ] + .spacing(6), + ) + .style(c_style(color_mantle, color_surface, 12.0)) + .padding(24) + .width(380) + .into() +} + fn friends_panel(state: &AppState) -> Element<'_, AppMessage> { let pal = state.config.theme.palette(); let color_crust = pal.crust; @@ -2255,14 +2299,28 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { // fixed-width Connect card would otherwise squeeze it until its node-ID // field and remove button clip away. `responsive` measures the available // width each layout pass and picks the orientation accordingly. + // The Recent-rooms card lives in the LEFT column under the Connect card + // (both are about getting into a room), so it has its own card and a + // growing history can't reflow the Create/Join controls. Friends stays on + // the right. Recents is omitted entirely (no stray gap) when empty. let body = responsive(move |size| { + let has_recents = !state.config.recents.is_empty(); let cards: Element = if size.width < 900.0 { - column![connect_card(state), friends_panel(state)] + let mut col = column![connect_card(state)] .spacing(20) - .align_x(iced::alignment::Horizontal::Center) - .into() + .align_x(iced::alignment::Horizontal::Center); + if has_recents { + col = col.push(recents_card(state)); + } + col.push(friends_panel(state)).into() } else { - row![connect_card(state), friends_panel(state)] + let mut left = column![connect_card(state)] + .spacing(20) + .align_x(iced::alignment::Horizontal::Center); + if has_recents { + left = left.push(recents_card(state)); + } + row![left, friends_panel(state)] .spacing(20) .align_y(iced::alignment::Vertical::Top) .into()