From 4d0600d26914c35063b7d463dee7baf871220598 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Fri, 10 Nov 2023 20:48:18 +0100 Subject: [PATCH] Move rooms which declare a space next to their space --- Cargo.toml | 8 +- src/app.rs | 12 ++- src/buffers/log.rs | 6 +- src/buffers/mod.rs | 121 +++++++++++++++++++++++++-- src/buffers/room.rs | 168 +++++++++++++++++++++----------------- src/components/buflist.rs | 15 +++- 6 files changed, 237 insertions(+), 93 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 77a6c88..811e7f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,10 +55,10 @@ smallvec = "1.11.1" eyeball = "0.8.7" # data structures observer returned by matrix-sdk-ui eyeball-im = "0.4.2" # immutable data structures observer returned by matrix-sdk-ui imbl = "2.0" # ditto -matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "91e7f2f7224b8ada17ab639d60da10dad98aeaf9", features = ["eyre", "markdown"] } -matrix-sdk-ui = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "91e7f2f7224b8ada17ab639d60da10dad98aeaf9" } -#matrix-sdk = { path = "../matrix-rust-sdk/crates/matrix-sdk", features = ["eyre", "markdown"] } -#matrix-sdk-ui = { path = "../matrix-rust-sdk/crates/matrix-sdk-ui" } +#matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "34060957855fdcb91af820a75df20774949e41be", features = ["eyre", "markdown"] } +#matrix-sdk-ui = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "34060957855fdcb91af820a75df20774949e41be" } +matrix-sdk = { path = "../matrix-rust-sdk/crates/matrix-sdk", features = ["eyre", "markdown"] } +matrix-sdk-ui = { path = "../matrix-rust-sdk/crates/matrix-sdk-ui" } # UI ansi-to-tui = "3.1.0" diff --git a/src/app.rs b/src/app.rs index 3a38940..67198ed 100644 --- a/src/app.rs +++ b/src/app.rs @@ -30,7 +30,7 @@ use tokio::sync::mpsc; use crate::{ action::Action, - buffers::{Buffers, LogBuffer, RoomBuffer}, + buffers::{BufferId, Buffers, LogBuffer, RoomBuffer}, commands::RataCommands, components::{Component, FpsCounter, Home}, config::Config, @@ -364,8 +364,14 @@ impl App { client: matrix_sdk::Client, sync_response: matrix_sdk::sync::SyncResponse, ) -> Result<()> { - let known_rooms: HashSet<&matrix_sdk::ruma::RoomId> = - self.buffers.iter().flat_map(|buf| buf.room_id()).collect(); + let known_rooms: HashSet = self + .buffers + .iter() + .flat_map(|buf| match buf.id() { + BufferId::Room(room_id) => Some(room_id), + _ => None, + }) + .collect(); let new_rooms: Vec<_> = sync_response .rooms .join diff --git a/src/buffers/log.rs b/src/buffers/log.rs index 75bf32b..6c0743e 100644 --- a/src/buffers/log.rs +++ b/src/buffers/log.rs @@ -25,7 +25,7 @@ use tokio::sync::mpsc::UnboundedReceiver; use tracing_error::ErrorLayer; use tracing_subscriber::prelude::*; -use super::{Buffer, BufferItem, BufferItemContent}; +use super::{Buffer, BufferId, BufferItem, BufferItemContent}; use crate::widgets::Prerender; /// Maximum number of log lines to be stored in memory @@ -60,6 +60,10 @@ impl Buffer for LogBuffer { "ratatrix".to_owned() } + fn id(&self) -> BufferId { + BufferId::Log + } + async fn poll_updates(&mut self) { let line = self .receiver diff --git a/src/buffers/mod.rs b/src/buffers/mod.rs index 90103be..89c5d35 100644 --- a/src/buffers/mod.rs +++ b/src/buffers/mod.rs @@ -20,12 +20,21 @@ use futures::StreamExt; use matrix_sdk::async_trait; use nonempty::NonEmpty; use ratatui::text::Text; +use std::collections::HashSet; mod log; pub use log::LogBuffer; mod room; pub use room::RoomBuffer; +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum BufferId { + /// The main/home buffer + Log, + /// Any Matrix room + Room(matrix_sdk::ruma::OwnedRoomId), +} + #[derive(Debug, Clone)] pub enum BufferItemContent<'buf> { Text(Text<'buf>), @@ -42,8 +51,10 @@ pub struct BufferItem<'buf> { pub trait Buffer: Send + Sync + memuse::DynamicUsage { /// A short human-readable name for the room, eg. to show in compact buflist fn short_name(&self) -> String; - /// If this is a room buffer, returns the associated room id. - fn room_id(&self) -> Option<&matrix_sdk::ruma::RoomId> { + /// Unique identifier of this buffer + fn id(&self) -> BufferId; + /// Identifier of this buffer's parent buffer, if any. + fn parent(&self) -> Option { None } /// Returns if there are any updates to apply. @@ -63,14 +74,14 @@ pub trait Buffer: Send + Sync + memuse::DynamicUsage { } pub struct Buffers { - buffers: NonEmpty>, + buffers: Vec>, active_index: usize, } impl Buffers { pub fn new(initial_buffer: Box) -> Self { Self { - buffers: NonEmpty::new(initial_buffer), + buffers: vec![initial_buffer], active_index: 0, } } @@ -83,6 +94,14 @@ impl Buffers { .next() .await .expect("poll_updates reached the end of the never-ending stream"); + + // Reorder buffers in case we just got an update on space relationships + // FIXME: do this only when needed + let mut buffers = Vec::new(); + std::mem::swap(&mut self.buffers, &mut buffers); + for buf in buffers.into_iter() { + self.push(buf); + } } pub fn len(&self) -> usize { @@ -97,8 +116,98 @@ impl Buffers { self.buffers.iter_mut() } - pub fn push(&mut self, buffer: Box) { - self.buffers.push(buffer) + /// `O(self.len())` if the buffer has a parent + pub fn push(&mut self, new_buffer: Box) { + let new_buffer_id = new_buffer.id(); + + match new_buffer + .parent() + .and_then(|parent_id| self.buffers.iter().position(|buf| buf.id() == parent_id)) + { + None => self.buffers.push(new_buffer), + Some(parent_position) => { + // iterator through buffers after the parent, and record on the stack the chain + // of successor from the parent to the current buffer. As soon as we see a buffer + // with a parent not on the stack (or with no parent), it means we left the subtree, + // and should insert the new buffer there. + let mut stack = vec![self.buffers[parent_position].id()]; + let mut new_buffer = Some(new_buffer); + for (i, buf) in self.buffers.iter().enumerate().skip(parent_position + 1) { + match buf.parent() { + None => {}, // root buffer + Some(parent_id) => { + match stack.iter().position(|id| *id == parent_id) { + None => {}, // parent is not in the subtree + Some(parent_position) => { + // parent is in the subtree + stack.truncate(parent_position + 1); + stack.push(buf.id()); + continue; + }, + } + }, + } + // if we are here, it means buf.parent() is not in the subtree (or buf is orphan) + // so we should insert the new buffer at this position + self + .buffers + .insert(i, new_buffer.take().expect("new_buffer was already taken")); + break; + } + + if let Some(new_buffer) = new_buffer { + // The parent's subtree was the last subtree + self.buffers.push(new_buffer); + } + }, + } + + // Find existing children and move them after this buffer (while preserving their + // relative order. + + // 1. collect the set of children + let mut children_ids = HashSet::new(); + children_ids.insert(new_buffer_id.clone()); + let mut added_children_ids = true; + while added_children_ids { + added_children_ids = false; + for buf in self.buffers.iter() { + if children_ids.contains(&buf.id()) { + // Already collected + continue; + } + if let Some(parent) = buf.parent() { + if children_ids.contains(&parent) { + children_ids.insert(buf.id()); + } + } + } + } + children_ids.remove(&new_buffer_id); + + // 2. pop them from the list of buffers + let mut all_buffers = Vec::new(); + let mut child_buffers = Vec::new(); + std::mem::swap(&mut all_buffers, &mut self.buffers); + for buf in all_buffers.into_iter() { + if children_ids.contains(&buf.id()) { + child_buffers.push(buf) + } else { + self.buffers.push(buf); + } + } + + // 3. split the set of buffers, and insert the children after the new buffer + let after_children = self.buffers.split_off( + self + .buffers + .iter() + .position(|buf| buf.id() == new_buffer_id) + .expect("new buffer disappeared") + + 1, + ); + self.buffers.extend(child_buffers.into_iter()); + self.buffers.extend(after_children); } pub fn active_index(&self) -> usize { diff --git a/src/buffers/room.rs b/src/buffers/room.rs index 4391558..ff379e7 100644 --- a/src/buffers/room.rs +++ b/src/buffers/room.rs @@ -25,6 +25,7 @@ use futures::stream::FuturesUnordered; use futures::{FutureExt, Stream, StreamExt}; use itertools::Itertools; use matrix_sdk::async_trait; +use matrix_sdk::room::ParentSpace; use matrix_sdk::ruma::{OwnedRoomId, RoomId}; use matrix_sdk::{Client, DisplayName, Room, RoomInfo}; use matrix_sdk_ui::timeline::{ @@ -35,7 +36,7 @@ use memuse::DynamicUsage; use ratatui::text::Text; use smallvec::SmallVec; -use super::{Buffer, BufferItem, BufferItemContent}; +use super::{Buffer, BufferId, BufferItem, BufferItemContent}; use crate::widgets::Prerender; /// Like [`BufferItemContent`] but owned. @@ -329,6 +330,7 @@ pub struct RoomBuffer { room_id: OwnedRoomId, initialized_roominfo: bool, + parent: Option, display_name: Option, // It's unlikely users will join the same room with more than one account; @@ -359,6 +361,7 @@ impl RoomBuffer { let mut self_ = RoomBuffer { room_id, initialized_roominfo: false, + parent: None, display_name: None, buffers: SmallVec::new(), }; @@ -397,6 +400,54 @@ impl RoomBuffer { }); Ok(()) } + + async fn update_room_info(&mut self, room: &Room) { + (self.parent, self.display_name) = tokio::join!( + async { + match room.parent_spaces().await { + Ok(parents) => { + parents + .flat_map_unordered(None, |parent| { + futures::stream::iter(match parent { + ParentSpace::Reciprocal(space) | ParentSpace::WithPowerlevel(space) => { + Some(space.room_id().to_owned()) + }, + ParentSpace::Unverifiable(_) | ParentSpace::Illegitimate(_) => None, + }) + }) + .next() // Get the first one to be ready. TODO: take the canonical space + .await + }, + Err(e) => { + tracing::error!("Failed to get parent spaces of {}: {:?}", self.room_id, e); + None + }, + } + }, + async { + match room.display_name().await { + Ok(dn) => { + tracing::debug!("Setting display name for {}: {}", self.room_id, dn); + Some(dn) + }, + Err(e) => { + tracing::error!( + "Error while resolving display name for {}: {}", + self.room_id, + e + ); + None + }, + } + } + ); + if let Some(parent) = self.parent.as_ref() { + tracing::debug!("{} has parent {}", self.room_id, parent); + } else { + tracing::debug!("{} has no parent", self.room_id); + } + self.initialized_roominfo = true; + } } #[async_trait] @@ -419,91 +470,54 @@ impl Buffer for RoomBuffer { }) } - fn room_id(&self) -> Option<&RoomId> { - Some(&self.room_id) + fn id(&self) -> BufferId { + BufferId::Room(self.room_id.to_owned()) + } + + fn parent(&self) -> Option { + self + .parent + .as_ref() + .map(|parent| BufferId::Room(parent.to_owned())) } async fn poll_updates(&mut self) { let room = if self.initialized_roominfo { - None + let mut roominfo_update = self + .buffers + .iter_mut() + .map(|buf| async { + let roominfo = buf.poll_updates().await; + (buf, roominfo) + }) + .collect::>(); + + let Some((buf, roominfo)) = roominfo_update.next().await else { + return; + }; + let Some(roominfo) = roominfo else { + return; + }; + let Some(room) = buf.client.get_room(&self.room_id) else { + return; + }; + + room } else { - Some( - self - .buffers - .first() - .unwrap_or_else(|| panic!("No sub-buffer for {}", self.room_id)) - .client - .get_room(&self.room_id) - .unwrap_or_else(|| panic!("Room {} disappeared", self.room_id)), - ) - }; - let mut roominfo_update = self - .buffers - .iter_mut() - .map(|buf| async { - let roominfo = buf.poll_updates().await; - (buf, roominfo) - }) - .collect::>(); - - let res = if self.initialized_roominfo { - roominfo_update.next().await - } else { - let room = room.unwrap(); // Set above iff !initialized_roominfo - - // Poll both roominfo_update and the display name, so we start getting new messages - // early if the user is in a hurry - tokio::select! { - biased; - dn = room.display_name() => { - match dn { - Ok(dn) => { - tracing::debug!("Initialized display name for {}: {}", self.room_id, dn); - self.display_name = Some(dn); - }, - Err(e) => { - tracing::error!( - "Error while resolving initial display name for {}: {}", - self.room_id, - e - ); - }, - }; - self.initialized_roominfo = true; - None - } - res = roominfo_update.next() => { res } - } - }; - let Some((buf, roominfo)) = res else { - return; - }; - let Some(roominfo) = roominfo else { - return; - }; - let Some(room) = buf.client.get_room(&self.room_id) else { - return; + self + .buffers + .first() + .unwrap_or_else(|| panic!("No sub-buffer for {}", self.room_id)) + .client + .get_room(&self.room_id) + .expect("Room not found in first client") }; - // This blocks any other update to the room while matrix-sdk computes the display - // name. Let's pretend it's a feature. (Although it's probably pretty bad when + // This blocks any other update to the room while matrix-sdk computes the display name + // and parent space. Let's pretend it's a feature. (Although it's probably pretty bad when // joined to the room with multiple clients and they all get the same update and // have to resolve the name one by one...) - self.display_name = match room.display_name().await { - Ok(dn) => { - tracing::debug!("Setting display name for {}: {}", self.room_id, dn); - Some(dn) - }, - Err(e) => { - tracing::error!( - "Error while resolving display name for {}: {}", - self.room_id, - e - ); - None - }, - }; - self.initialized_roominfo = true; + self.update_room_info(&room).await } fn content<'a>(&'a self) -> Box> + 'a> { diff --git a/src/components/buflist.rs b/src/components/buflist.rs index 5a29f61..2c5136b 100644 --- a/src/components/buflist.rs +++ b/src/components/buflist.rs @@ -65,25 +65,36 @@ impl Component for Buflist { ) -> Result<()> { let num_digits = u32::min(5, buffers.len().ilog10() + 1) as usize; let right_pad = " ".repeat(area.width.into()); + let mut stack = Vec::new(); // List of parent buffers of the current one frame.render_widget( Paragraph::new( buffers .iter() .enumerate() .map(|(i, buf)| { - let buf_number = format!("{}.", i+1); + match buf.parent() { + Some(parent) => { + stack.truncate(stack.iter().position(|id| *id == parent).unwrap_or(0) + 1) + }, + None => stack.clear(), + } + stack.push(buf.id()); + let buf_number = format!("{}.", i + 1); let mut base_style = Style::default(); if i == buffers.active_index() { base_style = base_style.on_blue(); } + let tree_pad = " ".repeat(stack.len() - 1); let buf_number_style = base_style.green(); let left_pad = " ".repeat((num_digits + 1).saturating_sub(buf_number.len())); vec![ Span::styled(left_pad, base_style), Span::styled(buf_number, buf_number_style), + Span::styled(tree_pad, base_style), Span::styled(buf.short_name(), base_style), Span::styled(right_pad.clone(), base_style), - ].into() + ] + .into() }) .collect::>>(), )