diff --git a/Cargo.toml b/Cargo.toml index 5ea97f4..a9248cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,9 @@ tracing-subscriber = { version = "0.2", features = ["parking_lot"] } [dependencies.matrix-sdk] git = "https://github.com/matrix-org/matrix-rust-sdk" -rev = "bca7f41" +rev = "d9e5a17" default_features = false -features = ["encryption", "sqlite_cryptostore", "messages", "rustls-tls"] +features = ["encryption", "sqlite_cryptostore", "messages", "rustls-tls", "unstable-synapse-quirks"] + +[profile.release] +lto = "thin" diff --git a/README.md b/README.md index 5b701ff..3c3d3d7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,37 @@ # retrix -Tiny matrix client \ No newline at end of file +Retrix is a lightweight matrix client built with [iced] and [matrix-rust-sdk]. + +The project is currently in early stages, and is decidedly not feature complete. Also note that both iced and matrix-sdk are somewhat unstable and under very rapid development, which means that there might be functionality that's broken or can't be implemented that I don't have direct influence over. + +# Features +- [x] Rooms + - [x] List rooms + - [ ] Join rooms + - [ ] Explore public room list + - [ ] Create room +- [ ] Communities +- [x] Messages + - [x] Plain text + - [ ] Formatted text (waiting on iced, markdown will be shown raw) + - [ ] Stickers + - [ ] Images + - [ ] Audio + - [ ] Video + - [ ] Location +- [x] E2E Encryption + - [x] Import key export + - [x] Receiving verification start + - [ ] Receiving verification request (waiting on matrix-sdk) +- [ ] Account settings + - [ ] Device management + - [ ] Change password +- [x] Profile settings + - [x] Display name + - [ ] Avatar + +## Things I (currently) don't intend to implement +- VoIP Calls + +[iced]: https://github.com/hecrj/iced +[matrix-rust-sdk]: https://github.com/matrix-org/matrix-rust-sdk diff --git a/src/ui.rs b/src/ui.rs index 66bd030..0cac04b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,4 +1,7 @@ -use std::collections::{BTreeMap, HashMap}; +use std::{ + collections::{BTreeMap, HashMap, VecDeque}, + time::SystemTime, +}; use futures::executor::block_on; use iced::{ @@ -10,13 +13,30 @@ use matrix_sdk::{ events::{ key::verification::cancel::CancelCode as VerificationCancelCode, room::message::MessageEventContent, AnyMessageEventContent, - AnyPossiblyRedactedSyncMessageEvent, AnySyncMessageEvent, AnyToDeviceEvent, + AnyPossiblyRedactedSyncMessageEvent, AnyRoomEvent, AnyStateEvent, AnySyncMessageEvent, + AnyToDeviceEvent, }, - identifiers::RoomId, + identifiers::{RoomAliasId, RoomId, UserId}, }; use crate::matrix; +pub trait AnyRoomEventExt { + fn origin_server_ts(&self) -> SystemTime; +} + +impl AnyRoomEventExt for AnyRoomEvent { + fn origin_server_ts(&self) -> SystemTime { + match self { + AnyRoomEvent::Message(e) => e.origin_server_ts(), + AnyRoomEvent::State(e) => e.origin_server_ts(), + AnyRoomEvent::RedactedMessage(e) => e.origin_server_ts(), + AnyRoomEvent::RedactedState(e) => e.origin_server_ts(), + } + .to_owned() + } +} + /// View for the login prompt #[derive(Debug, Clone, Default)] pub struct PromptView { @@ -147,6 +167,99 @@ pub enum RoomSorting { Alphabetic, } +/// Data for en entry in the room list +#[derive(Clone, Debug)] +pub struct RoomEntry { + /// Cached calculated name + name: String, + /// Room topic + topic: String, + /// Canonical alias + alias: Option, + /// Defined display name + display_name: Option, + /// Person we're in a direct message with + direct: Option, + /// Button to select the room + button: iced::button::State, + /// Most recent activity in the room + updated: std::time::SystemTime, + /// Cache of messages + messages: MessageBuffer, +} + +impl RoomEntry { + /// Recalculate displayname + pub fn update_display_name(&mut self, id: &RoomId) { + self.name = if let Some(ref name) = self.display_name { + name.to_owned() + } else if let Some(ref user) = self.direct { + user.to_string() + } else if let Some(ref alias) = self.alias { + alias.to_string() + } else { + id.to_string() + }; + } +} + +impl Default for RoomEntry { + fn default() -> Self { + Self { + name: Default::default(), + topic: String::new(), + alias: None, + display_name: None, + direct: None, + button: Default::default(), + updated: std::time::SystemTime::UNIX_EPOCH, + messages: Default::default(), + } + } +} + +impl RoomEntry { + fn update_time(&mut self) { + self.updated = self.messages.update_time(); + } +} + +#[derive(Clone, Debug, Default)] +pub struct MessageBuffer { + messages: VecDeque, + /// Token for the start of the messages we have + start: String, + /// Token for the end of the messages we have + end: String, +} + +impl MessageBuffer { + /// Sorts the messages by send time + fn sort(&mut self) { + self.messages + .make_contiguous() + .sort_unstable_by(|a, b| a.origin_server_ts().cmp(&b.origin_server_ts()).reverse()) + } + + /// Gets the send time of the most recently sent message + fn update_time(&self) -> SystemTime { + match self.messages.back() { + Some(message) => message.origin_server_ts(), + None => SystemTime::UNIX_EPOCH, + } + } + /// Insert a message that's probably the most recent + pub fn push_back(&mut self, event: AnyRoomEvent) { + self.messages.push_back(event); + self.sort(); + } + + pub fn push_front(&mut self, event: AnyRoomEvent) { + self.messages.push_front(event); + self.sort(); + } +} + /// Main view after successful login #[derive(Debug, Clone)] pub struct MainView { @@ -165,8 +278,7 @@ pub struct MainView { sas: Option, /// Whether to sort rooms alphabetically or by activity sorting: RoomSorting, - rooms: BTreeMap, - messages: BTreeMap, + rooms: BTreeMap, /// Room list entry/button to select room buttons: HashMap, @@ -201,10 +313,9 @@ impl MainView { message_scroll: Default::default(), message_input: Default::default(), buttons: Default::default(), - messages: Default::default(), draft: String::new(), send_button: Default::default(), - sorting: RoomSorting::Recent, + sorting: RoomSorting::Alphabetic, sas_accept_button: Default::default(), sas_deny_button: Default::default(), } @@ -275,14 +386,11 @@ impl MainView { m.origin_server_ts() } }) - .max() + .min() .copied(); match time { Some(time) => time, - None => { - println!("couldn't get time"); - std::time::SystemTime::now() - } + None => std::time::SystemTime::now(), } }), RoomSorting::Alphabetic => list.sort_by_cached_key(|id| { @@ -369,6 +477,9 @@ impl MainView { if let Some(ref sas) = self.sas { let device = sas.other_device(); let sas_row = match sas.emoji() { + _ if sas.is_done() => { + Row::new().push(Text::new("Verification complete").width(Length::Fill)) + } Some(emojis) => { let mut row = Row::new().push(Text::new("Verify emojis match:")); for (emoji, name) in emojis.iter() { @@ -477,7 +588,7 @@ pub enum Message { LoginFailed(String), // Main state messages - ResetRooms(BTreeMap), + ResetRooms(BTreeMap), SelectRoom(RoomId), /// Set error message ErrorMessage(String), @@ -732,19 +843,49 @@ impl Application for Retrix { *self = Retrix::Prompt(view); } Message::LoggedIn(client, session) => { - *self = Retrix::LoggedIn(MainView::new(client, session)); - /*let client = client.clone(); + *self = Retrix::LoggedIn(MainView::new(client.clone(), session)); return Command::perform( async move { - let mut rooms = BTreeMap::new(); + let mut rooms: BTreeMap = BTreeMap::new(); for (id, room) in client.joined_rooms().read().await.iter() { - let name = room.read().await.display_name(); - rooms.insert(id.to_owned(), name); + let room = room.read().await; + let entry = rooms.entry(id.clone()).or_default(); + + entry.direct = room.direct_target.clone(); + // Display name calculation for DMs is bronk so we're doing it + // ourselves + match entry.direct { + Some(ref direct) => { + let request = matrix_sdk::api::r0::profile::get_display_name::Request::new(direct); + if let Ok(response) = client.send(request).await { + if let Some(name) = response.displayname { + entry.name = name; + } + } + } + None => entry.name = room.display_name(), + } + let messages = room + .messages + .iter() + .cloned() + .map(|event| match event { + AnyPossiblyRedactedSyncMessageEvent::Redacted(e) => { + AnyRoomEvent::RedactedMessage( + e.into_full_event(id.clone()), + ) + } + AnyPossiblyRedactedSyncMessageEvent::Regular(e) => { + AnyRoomEvent::Message(e.into_full_event(id.clone())) + } + }) + .collect(); + entry.messages.messages = messages; } rooms }, - |rooms| Message::ResetRooms(rooms), - );*/ + Message::ResetRooms, + ); } _ => (), }, @@ -756,7 +897,30 @@ impl Application for Retrix { Message::ResetRooms(r) => view.rooms = r, Message::SelectRoom(r) => view.selected = Some(r), Message::Sync(event) => match event { - matrix::Event::Room(_) => (), + matrix::Event::Room(event) => match event { + AnyRoomEvent::Message(event) => { + let room = view.rooms.entry(event.room_id().clone()).or_default(); + room.messages + .push_back(AnyRoomEvent::Message(event.clone())); + } + AnyRoomEvent::State(event) => match event { + AnyStateEvent::RoomCanonicalAlias(alias) => { + let room = view.rooms.entry(alias.room_id).or_default(); + room.alias = alias.content.alias; + } + AnyStateEvent::RoomName(name) => { + let room = view.rooms.entry(name.room_id).or_default(); + room.display_name = name.content.name().map(String::from); + } + AnyStateEvent::RoomTopic(topic) => { + let room = view.rooms.entry(topic.room_id).or_default(); + } + any => { + let room = view.rooms.entry(any.room_id().clone()).or_default(); + } + }, + _ => (), + }, matrix::Event::ToDevice(event) => match event { AnyToDeviceEvent::KeyVerificationStart(start) => { let client = view.client.clone();