Move rooms which declare a space next to their space

This commit is contained in:
2023-11-10 20:48:18 +01:00
parent c5d54d26d2
commit 4d0600d269
6 changed files with 237 additions and 93 deletions

View File

@ -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"

View File

@ -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<matrix_sdk::ruma::OwnedRoomId> = 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

View File

@ -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

View File

@ -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<BufferId> {
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<Box<dyn Buffer>>,
buffers: Vec<Box<dyn Buffer>>,
active_index: usize,
}
impl Buffers {
pub fn new(initial_buffer: Box<dyn Buffer>) -> 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<dyn Buffer>) {
self.buffers.push(buffer)
/// `O(self.len())` if the buffer has a parent
pub fn push(&mut self, new_buffer: Box<dyn Buffer>) {
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 {

View File

@ -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<OwnedRoomId>,
display_name: Option<DisplayName>,
// 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<BufferId> {
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::<FuturesUnordered<_>>();
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::<FuturesUnordered<_>>();
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<dyn Iterator<Item = BufferItem<'a>> + 'a> {

View File

@ -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::<Vec<Line<'_>>>(),
)