feat(tui): scroll long names in every list and page through popups and chat

The sideways keys the results had (h/l, arrows, 0 and $) now scroll a
transfer's file name and a query too, each list on its own. The browse tree
and the room list take Home/End, PgUp/PgDn and the vim jumps like the main
lists, and a chat log, room or private, pages back through its history and
holds its place while new messages arrive; what arrives meanwhile counts as
unread until the reader returns to the end.
This commit is contained in:
Michel de Graaf
2026-09-07 08:16:20 +00:00
parent da8ab45303
commit 478d976776
18 changed files with 841 additions and 175 deletions
+15 -1
View File
@@ -1,6 +1,7 @@
use super::MainTui;
use super::input::{jumped, list_jump};
use crate::models::{BrowseStatus, files_under, find_node};
use ratatui::crossterm::event::{KeyCode, KeyEvent};
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::{thread, time::Duration};
/// How long to wait for a browse response before showing a timeout notice.
@@ -12,6 +13,11 @@ impl MainTui {
self.state.show_browse = false;
return;
}
let jump = list_jump(key, self.popup_page());
// Control combinations are only ever jumps: ctrl-d pages, d downloads.
if jump.is_none() && key.modifiers.contains(KeyModifiers::CONTROL) {
return;
}
// Tab management: switch between browsed users or close the active tab.
match key.code {
@@ -60,6 +66,14 @@ impl MainTui {
(rows, sel, row)
};
if let Some(jump) = jump {
if let Some(browse) = self.state.browse.active_tab_mut() {
browse.selected_row = jumped(sel, rows.len(), jump);
}
self.sync_browse_selection();
return;
}
// Downloads need `&self.client` free of the browse borrow.
match key.code {
KeyCode::Enter if !row.is_folder => {
+151 -47
View File
@@ -1,7 +1,10 @@
use super::MainTui;
use crate::models::{CommandBarMode, FocusedPane};
use crate::models::{CommandBarMode, FocusedPane, LogView};
use crate::ui::pane_block;
use crate::ui::panes::name_end_offset;
use crate::ui::panes::{
InfoSubject, name_end_offset, query_end_offset, selected_transfer,
transfer_name_end_offset, upload_display_name,
};
use ratatui::crossterm::event::{
KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
@@ -195,6 +198,14 @@ impl MainTui {
return;
}
// Paging keys move through the conversation's history.
if scroll_log(&mut self.state.chat_view, key) {
return;
}
if key.modifiers.contains(KeyModifiers::CONTROL) {
return;
}
match key.code {
KeyCode::Char('i' | 'q') | KeyCode::Esc => {
self.state.show_messages = false;
@@ -375,6 +386,15 @@ impl MainTui {
navigate_list(key, table, len, page)
}
/// Rows a popup shows at once: its inner height less a tab bar and a
/// compose line, which is what its list or log has left.
pub(super) fn popup_page(&self) -> isize {
let rows = self.state.popup_area.map_or(1, |area| {
pane_block(false).inner(area).height.saturating_sub(2)
});
isize::try_from(rows.max(1)).unwrap_or(isize::MAX)
}
/// Rows a pane shows at once: its inner height less the table header.
fn page_size(&self, pane: FocusedPane) -> isize {
let area = match pane {
@@ -389,7 +409,7 @@ impl MainTui {
}
fn handle_searches_input(&mut self, key: KeyEvent) {
if self.navigate_focused_list(key) {
if self.navigate_focused_list(key) || self.scroll_focused_name(key) {
return;
}
match key.code {
@@ -426,20 +446,10 @@ impl MainTui {
}
fn handle_results_input(&mut self, key: KeyEvent) {
if self.navigate_focused_list(key) {
if self.navigate_focused_list(key) || self.scroll_focused_name(key) {
return;
}
match key.code {
KeyCode::Right | KeyCode::Char('l') => {
self.scroll_names(NAME_SCROLL_STEP);
}
KeyCode::Left | KeyCode::Char('h') => {
self.scroll_names(-NAME_SCROLL_STEP);
}
KeyCode::Char('0') => self.state.results_name_offset = 0,
KeyCode::Char('$') => {
self.state.results_name_offset = self.highlighted_name_end();
}
KeyCode::Char(' ') => {
let index = self
.state
@@ -475,24 +485,82 @@ impl MainTui {
}
}
fn scroll_names(&mut self, delta: isize) {
let end = self.highlighted_name_end();
let current = self.state.results_name_offset.min(end);
self.state.results_name_offset =
current.saturating_add_signed(delta).min(end);
/// The keys that scroll the focused list's long column sideways, the
/// same in every list: a file name in Results and Downloads, the query
/// in Searches. Says whether `key` was one of them.
fn scroll_focused_name(&mut self, key: KeyEvent) -> bool {
match key.code {
KeyCode::Right | KeyCode::Char('l') => {
self.scroll_names(NAME_SCROLL_STEP);
}
KeyCode::Left | KeyCode::Char('h') => {
self.scroll_names(-NAME_SCROLL_STEP);
}
KeyCode::Char('0') => {
*self.state.name_offset_mut(self.state.focused_pane) = 0;
}
KeyCode::Char('$') => {
let end = self.highlighted_name_end();
*self.state.name_offset_mut(self.state.focused_pane) = end;
}
_ => return false,
}
true
}
fn scroll_names(&mut self, delta: isize) {
let end = self.highlighted_name_end();
let offset = self.state.name_offset_mut(self.state.focused_pane);
let current = (*offset).min(end);
*offset = current.saturating_add_signed(delta).min(end);
}
/// How far the highlighted row's long column can scroll before its end
/// is in view. Zero when nothing is highlighted or the pane is not drawn.
fn highlighted_name_end(&self) -> usize {
let Some(area) = self.state.results_pane_area else {
return 0;
};
self.highlighted_result()
.map_or(0, |file| name_end_offset(&file.filename, area))
match self.state.focused_pane {
FocusedPane::Searches => {
let Some(area) = self.state.searches_pane_area else {
return 0;
};
self.state
.searches_table_state
.selected()
.and_then(|row| self.state.searches.get(row))
.map_or(0, |search| query_end_offset(&search.query, area))
}
FocusedPane::Results => {
let Some(area) = self.state.results_pane_area else {
return 0;
};
self.highlighted_result()
.map_or(0, |file| name_end_offset(&file.filename, area))
}
FocusedPane::Downloads => {
let Some(area) = self.state.downloads_pane_area else {
return 0;
};
let shown = match selected_transfer(
self.state.downloads_table_state.selected(),
&self.state.downloads,
&self.state.uploads,
) {
Some(InfoSubject::Download(entry)) => {
entry.download.filename.as_str()
}
Some(InfoSubject::Upload(upload)) => {
upload_display_name(upload)
}
Some(InfoSubject::Result(_)) | None => return 0,
};
transfer_name_end_offset(shown, area)
}
}
}
fn handle_downloads_input(&mut self, key: KeyEvent) {
// The pane lists downloads first, then uploads; navigation spans both.
if self.navigate_focused_list(key) {
if self.navigate_focused_list(key) || self.scroll_focused_name(key) {
return;
}
match key.code {
@@ -539,9 +607,58 @@ impl MainTui {
}
}
/// The keys every list answers alike: a row at a time wrapping at either
/// end, a page or half a page at a time stopping there, and the two ends.
/// A move over a whole list at once: to either end, or by a number of rows
/// (negative is up).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ListJump {
First,
Last,
Rows(isize),
}
/// The keys every list and log answers alike for moving further than a row:
/// the two ends, a page, half a page. `page` is how many rows the list shows.
pub(super) fn list_jump(key: KeyEvent, page: isize) -> Option<ListJump> {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let half = (page / 2).max(1);
Some(match key.code {
KeyCode::Home | KeyCode::Char('g') if !ctrl => ListJump::First,
KeyCode::End | KeyCode::Char('G') if !ctrl => ListJump::Last,
KeyCode::PageDown => ListJump::Rows(page),
KeyCode::PageUp => ListJump::Rows(-page),
KeyCode::Char('f') if ctrl => ListJump::Rows(page),
KeyCode::Char('b') if ctrl => ListJump::Rows(-page),
KeyCode::Char('d') if ctrl => ListJump::Rows(half),
KeyCode::Char('u') if ctrl => ListJump::Rows(-half),
_ => return None,
})
}
/// Apply the jump keys to a log, moving by the screenful it last showed.
/// Says whether `key` was one of them.
pub(super) fn scroll_log(view: &mut LogView, key: KeyEvent) -> bool {
let page = isize::try_from(view.page()).unwrap_or(isize::MAX);
match list_jump(key, page) {
Some(ListJump::First) => view.to_oldest(),
Some(ListJump::Last) => view.to_newest(),
Some(ListJump::Rows(rows)) => view.scroll(rows),
None => return false,
}
true
}
/// `selected` moved by `jump` over a list `len` long, stopping at the ends.
pub(super) fn jumped(selected: usize, len: usize, jump: ListJump) -> usize {
let last = len.saturating_sub(1);
match jump {
ListJump::First => 0,
ListJump::Last => last,
ListJump::Rows(rows) => selected.saturating_add_signed(rows).min(last),
}
}
/// The keys every list answers alike: a row at a time wrapping at either
/// end, and the jumps of [`list_jump`]. Says whether `key` was one of them.
fn navigate_list(
key: KeyEvent,
table: &mut TableState,
@@ -549,23 +666,16 @@ fn navigate_list(
page: isize,
) -> bool {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let half = (page / 2).max(1);
match key.code {
KeyCode::Up | KeyCode::Char('k') if !ctrl => cycle(table, len, false),
KeyCode::Down | KeyCode::Char('j') if !ctrl => cycle(table, len, true),
KeyCode::Home | KeyCode::Char('g') if !ctrl => {
select_row(table, len, 0);
}
KeyCode::End | KeyCode::Char('G') if !ctrl => {
select_row(table, len, len.saturating_sub(1));
}
KeyCode::PageDown => scroll_rows(table, len, page),
KeyCode::PageUp => scroll_rows(table, len, -page),
KeyCode::Char('f') if ctrl => scroll_rows(table, len, page),
KeyCode::Char('b') if ctrl => scroll_rows(table, len, -page),
KeyCode::Char('d') if ctrl => scroll_rows(table, len, half),
KeyCode::Char('u') if ctrl => scroll_rows(table, len, -half),
_ => return false,
_ => match list_jump(key, page) {
Some(jump) => {
let current = table.selected().unwrap_or(0);
select_row(table, len, jumped(current, len, jump));
}
None => return false,
},
}
true
}
@@ -590,9 +700,3 @@ fn select_row(table: &mut TableState, len: usize, target: usize) {
table.select(Some(target.min(len - 1)));
}
}
/// Move the selection `delta` rows, stopping at either end.
fn scroll_rows(table: &mut TableState, len: usize, delta: isize) {
let current = table.selected().unwrap_or(0);
select_row(table, len, current.saturating_add_signed(delta));
}
+205 -3
View File
@@ -1548,12 +1548,12 @@ mod tests {
let panes = row_with(&screen, "Panes");
assert!(!panes.contains("Searches"), "one column: {panes}");
assert!(screen.contains("focus a pane, hidden or not"), "{screen}");
assert!(!screen.contains("clear every finished one"), "{screen}");
assert!(!screen.contains("next room, chat or user"), "{screen}");
press(&mut tui, KeyCode::End);
let screen = screen_sized(&mut tui, 80, 24);
assert!(screen.contains("clear every finished one"), "{screen}");
assert!(!screen.contains("Tab / Shift-Tab"), "{screen}");
assert!(screen.contains("next room, chat or user"), "{screen}");
assert!(!screen.contains("next / previous pane"), "{screen}");
press(&mut tui, KeyCode::Home);
let screen = screen_sized(&mut tui, 80, 24);
@@ -1587,6 +1587,208 @@ mod tests {
assert_eq!(tui.state.focused_pane, FocusedPane::Results);
}
#[test]
fn l_and_h_scroll_a_transfer_name_and_a_query_too() {
let mut tui = furnished_tui();
tui.state.downloads[0].download.filename = "y".repeat(120);
tui.state.searches[0].query = "z".repeat(60);
let _ = screen_of(&mut tui);
tui.state.focused_pane = FocusedPane::Downloads;
press(&mut tui, KeyCode::Char('l'));
assert_eq!(tui.state.downloads_name_offset, 8);
press(&mut tui, KeyCode::Char('$'));
let end = tui.state.downloads_name_offset;
assert!(end > 8, "the end is past a step: {end}");
let screen = screen_of(&mut tui);
assert!(row_with(&screen, "…yyyy").contains("…yyyy"), "{screen}");
press(&mut tui, KeyCode::Char('0'));
assert_eq!(tui.state.downloads_name_offset, 0);
tui.state.focused_pane = FocusedPane::Searches;
press(&mut tui, KeyCode::Right);
assert_eq!(tui.state.searches_query_offset, 8);
press(&mut tui, KeyCode::End);
assert_eq!(
tui.state.searches_table_state.selected(),
Some(0),
"End is still a row key, $ is the name key"
);
press(&mut tui, KeyCode::Char('$'));
assert!(tui.state.searches_query_offset > 8);
let screen = screen_of(&mut tui);
assert!(screen.contains("…zzzz"), "{screen}");
assert!(
tui.state.results_name_offset == 0,
"each list scrolls on its own"
);
}
fn browsed(tui: &mut MainTui, files: usize) {
tui.state.browse.open("bob");
let listing = vec![soulseek_rs::SharedDirectory {
name: "Music".to_string(),
files: (0..files).map(|i| (format!("{i:02}.mp3"), 1)).collect(),
}];
tui.state
.browse
.active_tab_mut()
.expect("tab")
.load(&listing);
tui.state.show_browse = true;
let _ = screen_of(tui);
}
#[test]
fn page_keys_walk_the_browse_tree_and_ctrl_d_does_not_download() {
let session = Arc::new(TalkativeSession::default());
let mut tui = attach(session);
browsed(&mut tui, 40);
let rows = tui.state.browse.active_tab().expect("tab").rows().len();
assert_eq!(rows, 41, "the folder and its files");
press(&mut tui, KeyCode::End);
let selected = |tui: &MainTui| {
tui.state.browse.active_tab().expect("tab").selected_row
};
assert_eq!(selected(&tui), 40);
press(&mut tui, KeyCode::Home);
assert_eq!(selected(&tui), 0);
press(&mut tui, KeyCode::PageDown);
let page = selected(&tui);
assert!(page > 1 && page < 40, "a page of rows: {page}");
ctrl(&mut tui, 'd');
assert!(selected(&tui) > page, "ctrl-d pages");
assert!(
tui.state.downloads.is_empty()
&& tui.state.downloads_receiver_channel.is_none(),
"and does not download the folder"
);
press(&mut tui, KeyCode::Char('G'));
assert_eq!(selected(&tui), 40);
assert!(tui.state.show_browse, "still open");
}
fn in_a_room(tui: &mut MainTui, messages: usize) {
tui.state.rooms.apply_event(
soulseek_rs::RoomEvent::Joined {
room: "jazz".to_string(),
users: (0..30).map(|i| format!("user{i:02}")).collect(),
},
None,
);
tui.state.rooms.focus_or_open("jazz");
for i in 0..messages {
tui.state.rooms.apply_event(
soulseek_rs::RoomEvent::Message {
room: "jazz".to_string(),
username: "alice".to_string(),
message: format!("line {i:03}"),
},
Some("jazz"),
);
}
tui.state.show_rooms = true;
let _ = screen_of(tui);
}
#[test]
fn page_up_scrolls_a_room_log_back_and_end_returns_to_the_newest() {
let mut tui = with_session(TalkativeSession::default());
in_a_room(&mut tui, 120);
let screen = screen_of(&mut tui);
assert!(screen.contains("line 119"), "tails by default: {screen}");
assert!(!screen.contains("line 000"), "{screen}");
press(&mut tui, KeyCode::PageUp);
let screen = screen_of(&mut tui);
assert!(!screen.contains("line 119"), "scrolled back: {screen}");
press(&mut tui, KeyCode::Home);
let screen = screen_of(&mut tui);
assert!(screen.contains("line 000"), "the oldest: {screen}");
press(&mut tui, KeyCode::PageUp);
let screen = screen_of(&mut tui);
assert!(screen.contains("line 000"), "clamped at the top: {screen}");
press(&mut tui, KeyCode::End);
let screen = screen_of(&mut tui);
assert!(screen.contains("line 119"), "{screen}");
// The member list keeps its own keys, and ctrl-x is not x.
press(&mut tui, KeyCode::Down);
assert_eq!(tui.state.rooms.user_selected, 1);
ctrl(&mut tui, 'x');
assert_eq!(tui.state.rooms.open.len(), 1, "still in the room");
}
#[test]
fn page_keys_move_through_the_room_list() {
let mut tui = with_session(TalkativeSession::default());
tui.state.rooms.apply_event(
soulseek_rs::RoomEvent::List(
(0..60)
.map(|i| soulseek_rs::types::RoomInfo {
name: format!("room{i:02}"),
user_count: 60 - i,
})
.collect(),
),
None,
);
tui.state.show_rooms = true;
let _ = screen_of(&mut tui);
press(&mut tui, KeyCode::End);
assert_eq!(tui.state.rooms.list_selected, 59);
press(&mut tui, KeyCode::PageUp);
assert!(tui.state.rooms.list_selected < 59);
press(&mut tui, KeyCode::Char('g'));
assert_eq!(tui.state.rooms.list_selected, 0);
ctrl(&mut tui, 'f');
assert!(tui.state.rooms.list_selected > 1);
}
#[test]
fn page_up_scrolls_a_conversation_and_switching_chats_resets_it() {
let mut tui = tui((0..80)
.map(|i| ChatMessageDto {
peer: "bob".into(),
outgoing: i % 2 == 0,
text: format!("msg {i:03}"),
at: i,
})
.chain(std::iter::once(ChatMessageDto {
peer: "carol".into(),
outgoing: false,
text: "hello".into(),
at: 100,
}))
.collect());
tui.state.chat_peer = Some("bob".to_string());
tui.state.show_messages = true;
let screen = screen_of(&mut tui);
assert!(screen.contains("msg 079"), "{screen}");
press(&mut tui, KeyCode::PageUp);
let screen = screen_of(&mut tui);
assert!(!screen.contains("msg 079"), "scrolled back: {screen}");
press(&mut tui, KeyCode::Char('g'));
let screen = screen_of(&mut tui);
assert!(screen.contains("msg 000"), "{screen}");
press(&mut tui, KeyCode::Tab);
assert_eq!(tui.state.active_chat_peer(), Some("carol"));
assert!(
tui.state.chat_view.following(),
"a fresh chat starts at its end"
);
press(&mut tui, KeyCode::BackTab);
let screen = screen_of(&mut tui);
assert!(screen.contains("msg 079"), "{screen}");
}
#[test]
fn the_shortcut_bar_wraps_instead_of_cutting_keys_off() {
let mut tui = furnished_tui();
+25 -5
View File
@@ -50,6 +50,8 @@ const HELP_LEFT: &[(&str, &[(&str, &str)])] = &[
("Home End / g G", "first / last row"),
("PgUp PgDn / ^b ^f", "one page"),
("^u ^d", "half a page"),
("h l / ← →", "scroll a long name sideways"),
("0 / $", "its start / its end"),
],
),
];
@@ -71,8 +73,6 @@ const HELP_RIGHT: &[(&str, &[(&str, &str)])] = &[
("a / A", "select all / none"),
("Enter", "download the selection"),
("/", "filter, Enter keeps it, Esc clears"),
("h l / ← →", "scroll the file name"),
("0 / $", "start / end of the file name"),
("b", "browse the owner"),
("c", "chat rooms"),
],
@@ -87,6 +87,15 @@ const HELP_RIGHT: &[(&str, &[(&str, &str)])] = &[
("c", "clear every finished one"),
],
),
(
"Chat and browse popups",
&[
("PgUp PgDn / ^b ^f", "a page of messages, or of the tree"),
("^u ^d", "half a page of them"),
("Home End / g G", "oldest / newest, or first / last row"),
("Tab / Shift-Tab", "next room, chat or user"),
],
),
];
impl MainTui {
@@ -192,6 +201,7 @@ impl MainTui {
&self.state.searches,
&mut self.state.searches_table_state,
focused,
self.state.searches_query_offset,
);
}
FocusedPane::Results => {
@@ -207,6 +217,7 @@ impl MainTui {
&self.state.uploads,
&mut self.state.downloads_table_state,
focused,
self.state.downloads_name_offset,
);
}
}
@@ -329,6 +340,7 @@ impl MainTui {
// Browse tree overlays everything when open.
if self.state.show_browse && !self.state.browse.is_empty() {
let area = centered_rect(80, 80, frame.area());
self.state.popup_area = Some(area);
frame.render_widget(ratatui::widgets::Clear, area);
render_browse_pane(
frame,
@@ -347,11 +359,12 @@ impl MainTui {
// Chat rooms overlay everything when open.
if self.state.show_rooms {
let area = centered_rect(85, 80, frame.area());
self.state.popup_area = Some(area);
frame.render_widget(ratatui::widgets::Clear, area);
render_rooms_pane(
frame,
area,
&self.state.rooms,
&mut self.state.rooms,
&mut self.state.rooms_list_table_state,
);
}
@@ -430,12 +443,14 @@ impl MainTui {
);
}
fn render_messages_popup(&self, frame: &mut Frame) {
fn render_messages_popup(&mut self, frame: &mut Frame) {
// The per-conversation chat box supersedes the old flat message list;
// it renders the messages and the compose line itself.
let area = centered_rect(70, 60, frame.area());
frame.render_widget(ratatui::widgets::Clear, area);
render_chat_pane(frame, area, &self.state, &self.client.username());
self.state.popup_area = Some(area);
let own = self.client.username();
render_chat_pane(frame, area, &mut self.state, &own);
}
/// Context shortcuts for the chat-rooms popup.
@@ -467,6 +482,7 @@ impl MainTui {
}
RoomsView::Chat => vec![
("Enter", "say"),
("PgUp/PgDn", "scroll"),
("↑↓", "pick user"),
("b", "browse user"),
("m", "message user"),
@@ -520,6 +536,7 @@ impl MainTui {
} else {
vec![
("Enter", "type"),
("PgUp/PgDn", "scroll"),
("↑↓/Tab", "switch chat"),
("m", "new chat"),
("i/Esc", "close"),
@@ -530,6 +547,7 @@ impl MainTui {
} else if self.state.show_browse {
vec![
("↑↓", "move"),
("PgUp/PgDn", "page"),
("→←", "expand/collapse"),
("Enter", "open/download"),
("d", "download folder"),
@@ -579,6 +597,7 @@ impl MainTui {
("i", "inbox"),
("c", "chat"),
("b", "browse user"),
("h/l", "scroll query"),
],
FocusedPane::Results => vec![
("Space", "select"),
@@ -597,6 +616,7 @@ impl MainTui {
("d", "delete queued/done"),
("c", "clear finished"),
("b", "browse user"),
("h/l", "scroll name"),
],
};
keys.extend(self.pane_shortcuts());
+36 -7
View File
@@ -1,4 +1,5 @@
use super::MainTui;
use super::input::{jumped, list_jump, scroll_log};
use crate::models::RoomsView;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
@@ -38,6 +39,16 @@ impl MainTui {
fn handle_rooms_list_input(&mut self, key: KeyEvent) {
let len = self.state.rooms.filtered_rooms().len();
if let Some(jump) = list_jump(key, self.popup_page()) {
self.state.rooms.list_selected =
jumped(self.state.rooms.list_selected, len, jump);
return;
}
// Control combinations are only ever jumps; the letters below are
// not theirs to trigger.
if key.modifiers.contains(KeyModifiers::CONTROL) {
return;
}
match key.code {
// Esc peels back one level: clear a lingering filter first (as the
// title's "Esc: clear" promises), otherwise close the popup.
@@ -92,6 +103,16 @@ impl MainTui {
}
fn handle_rooms_chat_input(&mut self, key: KeyEvent) {
// Paging keys move through the room's history; the member list is a
// row at a time.
if let Some(view) = self.state.rooms.active_view_mut()
&& scroll_log(view, key)
{
return;
}
if key.modifiers.contains(KeyModifiers::CONTROL) {
return;
}
match key.code {
KeyCode::Char('q') => self.state.show_rooms = false,
KeyCode::Esc | KeyCode::Char('l') => {
@@ -208,15 +229,23 @@ impl MainTui {
self.state.rooms.composing = false;
}
/// The room whose newest messages are on screen, if any: the active one
/// in the chat view, unless the reader is held back in its history, when
/// what arrives still counts as unread.
pub(super) fn viewed_room(&self) -> Option<String> {
if !self.state.show_rooms || self.state.rooms.view != RoomsView::Chat {
return None;
}
self.state
.rooms
.active_room()
.filter(|room| room.view.following())
.map(|room| room.name.clone())
}
/// Drain chat-room events into the rooms state, tracking unread badges.
pub(super) fn poll_room_events(&mut self) {
let viewing = if self.state.show_rooms
&& self.state.rooms.view == RoomsView::Chat
{
self.state.rooms.active_room().map(|r| r.name.clone())
} else {
None
};
let viewing = self.viewed_room();
for event in self.client.take_room_events() {
self.state.rooms.apply_event(event, viewing.as_deref());
}