diff --git a/Cargo.toml b/Cargo.toml index 406bd6d..affd0c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,12 +9,12 @@ edition = "2018" [dependencies] anyhow = "1.0" async-stream = "0.3" +async-trait = "0.1" dirs-next = "2.0" futures = "0.3" iced = { git = "https://github.com/hecrj/iced", rev = "31522e3", features = ["debug", "image", "tokio"] } iced_futures = { git = "https://github.com/hecrj/iced", rev = "31522e3" } #iced_glow = { git = "https://github.com/hecrj/iced", rev = "31522e3", features = ["image"] } -#matrix-sdk-common-macros = { git = "https://github.com/matrix-org/matrix-rust-sdk", rev = "e65915e" } serde = { version = "1.0", features = ["derive"] } time = "0.2" tokio = { version = "1.0", features = ["sync"] } @@ -23,7 +23,7 @@ tracing-subscriber = { version = "0.2", features = ["parking_lot"] } [dependencies.matrix-sdk] git = "https://github.com/matrix-org/matrix-rust-sdk" -rev = "6435269" +rev = "40c53f0" default_features = false features = ["encryption", "sqlite_cryptostore", "messages", "rustls-tls", "unstable-synapse-quirks"] diff --git a/src/matrix.rs b/src/matrix.rs index d5ff589..3ee3ade 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -22,9 +22,9 @@ pub type Error = anyhow::Error; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Session { access_token: String, - user_id: UserId, - device_id: Box, - homeserver: String, + pub user_id: UserId, + pub device_id: Box, + pub homeserver: String, } impl From for matrix_sdk::Session { @@ -121,7 +121,7 @@ pub async fn restore_login(session: Session) -> Result<(Client, Session), Error> let client = client(url)?; client.restore_login(session.clone().into()).await?; - //client.sync_once(SyncSettings::new()).await?; + client.sync_once(SyncSettings::new()).await?; Ok((client, session)) } @@ -162,7 +162,7 @@ fn write_session(session: &Session) -> Result<(), Error> { pub fn parse_mxc(url: &str) -> Result<(Box, String), Error> { let url = Url::parse(&url)?; anyhow::ensure!(url.scheme() == "mxc", "Not an mxc url"); - let host = url.host_str().ok_or(anyhow::anyhow!("url"))?; + let host = url.host_str().ok_or_else(|| anyhow::anyhow!("url"))?; let server_name: Box = <&ServerName>::try_from(host)?.into(); let path = url.path_segments().and_then(|mut p| p.next()); match path { @@ -190,6 +190,45 @@ pub enum Event { Token(String), } +/*pub enum Sync { + MessageInvited() + MessageJoined(RoomId, AnyRoomEvent), + MessageLeft(RoomId, AnyRoomEvent), +} + +struct Emitter { + client: Client, + sender: tokio::sync::mpsc::Sender, +} + +#[async_trait] +impl matrix_sdk::EventEmitter for Emitter { + async fn on_room_message( + &self, + room: RoomState, + message: &SyncMessageEvent, + ) { + let id = room.room_id().to_owned(); + let full = AnyRoomEvent::Message( + AnyMessageEvent::RoomMessage( + message + .to_owned() + .into_full_event(id.clone()), + ), + ); + if let RoomState::Joined(room) = room { + self.sender + .send(Sync::MessageJoined(id, full)) + .await + .ok(); + } + } + + async fn on_room_member(&self, room: RoomState, member: &SyncStateEvent) { + + } +}*/ + impl iced_futures::subscription::Recipe for MatrixSync where H: std::hash::Hasher, @@ -214,11 +253,18 @@ where .sync_with_callback( SyncSettings::new() .token(client.sync_token().await.unwrap()) - .timeout(Duration::from_secs(90)) - .full_state(true), + .timeout(Duration::from_secs(30)), |response| async { - sender.send(Event::Token(response.next_batch)).ok(); + //sender.send(Event::Token(response.next_batch)).ok(); for (id, room) in response.rooms.join { + for event in room.state.events { + let id = id.clone(); + sender + .send(Event::Room(AnyRoomEvent::State( + event.into_full_event(id), + ))) + .ok(); + } for event in room.timeline.events { let id = id.clone(); let event = match event { diff --git a/src/ui.rs b/src/ui.rs index b9fb561..bf10b32 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -5,8 +5,8 @@ use std::{ use futures::executor::block_on; use iced::{ - Align, Application, Button, Column, Command, Container, Element, Length, Row, Rule, Scrollable, - Subscription, Text, TextInput, + Align, Application, Button, Column, Command, Container, Element, Image, Length, Row, Rule, + Scrollable, Subscription, Text, TextInput, }; use matrix_sdk::{ api::r0::{ @@ -15,7 +15,10 @@ use matrix_sdk::{ }, events::{ key::verification::cancel::CancelCode as VerificationCancelCode, - room::{member::MembershipState, message::MessageEventContent}, + room::{ + member::MembershipState, + message::{MessageEventContent, Relation, TextMessageEventContent}, + }, AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent, AnyStateEvent, AnyToDeviceEvent, }, identifiers::{EventId, RoomAliasId, RoomId, UserId}, @@ -47,6 +50,8 @@ pub struct RoomEntry { pub alias: Option, /// Defined display name pub display_name: Option, + /// mxc url for the rooms avatar + pub avatar: Option, /// Person we're in a direct message with pub direct: Option, /// Cache of messages @@ -54,12 +59,13 @@ pub struct RoomEntry { } impl RoomEntry { - pub fn from_sdk(room: &matrix_sdk::JoinedRoom) -> Self { + pub async fn from_sdk(room: &matrix_sdk::JoinedRoom) -> Self { Self { direct: room.direct_target(), - name: block_on(async { room.display_name().await }), + name: room.display_name().await, topic: room.topic().unwrap_or_default(), alias: room.canonical_alias(), + avatar: room.avatar_url(), ..Default::default() } } @@ -99,9 +105,30 @@ impl MessageBuffer { }; } + fn remove(&mut self, id: &EventId) { + self.messages.retain(|e| e.event_id() != id); + self.known_ids.remove(&id); + } + /// Add a message to the buffer. pub fn push(&mut self, event: AnyRoomEvent) { self.known_ids.insert(event.event_id().clone()); + if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage( + matrix_sdk::events::MessageEvent { + content: + MessageEventContent::Text(TextMessageEventContent { + relates_to: Some(Relation::Replacement(ref replacement)), + .. + }), + .. + }, + )) = event + { + self.remove(&replacement.event_id); + } + if let AnyRoomEvent::Message(AnyMessageEvent::RoomRedaction(ref redaction)) = event { + self.remove(&redaction.redacts); + } self.messages.push(event); self.sort(); self.update_time(); @@ -111,6 +138,23 @@ impl MessageBuffer { pub fn append(&mut self, mut events: Vec) { events.retain(|e| !self.known_ids.contains(e.event_id())); for event in events.iter() { + // Handle replacement + if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage( + matrix_sdk::events::MessageEvent { + content: + MessageEventContent::Text(TextMessageEventContent { + relates_to: Some(Relation::Replacement(replacement)), + .. + }), + .. + }, + )) = event + { + self.remove(&replacement.event_id); + } + if let AnyRoomEvent::Message(AnyMessageEvent::RoomRedaction(redaction)) = event { + self.remove(&redaction.redacts); + } self.known_ids.insert(event.event_id().clone()); } self.messages.append(&mut events); @@ -216,6 +260,23 @@ impl MainView { } } + pub fn room<'a>(&'a self, id: &RoomId) -> Result<&'a RoomEntry, Command> { + match self.rooms.get(&id) { + Some(room) => Ok(room), + None => { + let id = id.clone(); + let client = self.client.clone(); + let cmd = async move { + let room = client.get_joined_room(&id).unwrap(); + let entry = RoomEntry::from_sdk(&room).await; + Message::ResetRoom(id, entry) + } + .into(); + Err(cmd) + } + } + } + pub fn view(&mut self) -> Element { // If settings view is open, display that instead if let Some(ref mut settings) = self.settings_view { @@ -228,14 +289,23 @@ impl MainView { .height(Length::Fill) .scrollbar_width(5); + let client = &self.client; + let rooms = &self.rooms; // Group by DM and group conversation #[allow(clippy::type_complexity)] let (mut dm_rooms, mut group_rooms): ( Vec<(&RoomId, &RoomEntry)>, Vec<(&RoomId, &RoomEntry)>, - ) = self - .rooms + ) = rooms .iter() + // Hide if we're in the room the tombstone points to + .filter(|(id, _)| { + !client + .get_joined_room(id) + .and_then(|j| j.tombstone()) + .map(|t| rooms.contains_key(&t.replacement_room)) + .unwrap_or(false) + }) .partition(|(_, room)| room.direct.is_some()); // Sort for list in [&mut dm_rooms, &mut group_rooms].iter_mut() { @@ -243,7 +313,6 @@ impl MainView { RoomSorting::Alphabetic => { list.sort_unstable_by_key(|(_, room)| room.name.to_uppercase()) } - // TODO: fix this RoomSorting::Recent => list.sort_unstable_by(|(_, a), (_, b)| { a.messages.updated.cmp(&b.messages.updated).reverse() }), @@ -255,21 +324,32 @@ impl MainView { self.group_buttons .resize_with(group_rooms.len(), Default::default); // Create buttons + let images = &self.images; let dm_buttons: Vec> = self .dm_buttons .iter_mut() .enumerate() .map(|(idx, button)| { // TODO: highlight selected - let (id, room) = dm_rooms[idx]; + let (id, room) = unsafe { dm_rooms.get_unchecked(idx) }; let name = if room.name.is_empty() { "Missing name" } else { &room.name }; - Button::new(button, Text::new(name)) + let mut row = Row::new().align_items(Align::Center); + if let Some(ref url) = room.avatar { + if let Some(handle) = images.get(url) { + row = row.push( + Image::new(handle.clone()) + .width(20.into()) + .height(20.into()), + ); + } + } + Button::new(button, row.push(Text::new(name))) .width(300.into()) - .on_press(Message::SelectRoom(id.to_owned())) + .on_press(Message::SelectRoom(id.to_owned().to_owned())) }) .collect(); let room_buttons: Vec> = self @@ -277,10 +357,25 @@ impl MainView { .iter_mut() .enumerate() .map(|(idx, button)| { - let (id, room) = group_rooms[idx]; - Button::new(button, Text::new(&room.name)) + let (id, room) = unsafe { group_rooms.get_unchecked(idx) }; + let name = if room.name.is_empty() { + "Missing name" + } else { + &room.name + }; + let mut row = Row::new().align_items(Align::Center); + if let Some(ref url) = room.avatar { + if let Some(handle) = images.get(url) { + row = row.push( + Image::new(handle.clone()) + .width(20.into()) + .height(20.into()), + ); + } + } + Button::new(button, row.push(Text::new(name))) .width(300.into()) - .on_press(Message::SelectRoom(id.to_owned())) + .on_press(Message::SelectRoom(id.to_owned().to_owned())) }) .collect(); // Add buttons to container @@ -322,11 +417,20 @@ impl MainView { room.name.clone() }; + let mut title_row = Row::new().align_items(Align::Center); + if let Some(handle) = room.avatar.as_deref().and_then(|a| images.get(a)) { + title_row = title_row.push( + Image::new(handle.to_owned()) + .width(24.into()) + .height(24.into()), + ); + } message_col = message_col - .push(Text::new(title).size(25)) + .push(title_row.push(Text::new(title).size(25))) .push(Rule::horizontal(2)); let mut scroll = Scrollable::new(&mut self.message_scroll) .scrollbar_width(2) + .spacing(4) .height(Length::Fill); // Backfill button or loading message let backfill: Element<_> = if room.messages.loading { @@ -348,16 +452,28 @@ impl MainView { .into() }; scroll = scroll.push(Container::new(backfill).width(Length::Fill).center_x()); + // mxid of most recent sender + let mut last_sender: Option = None; + // Rendered display name of most recent sender + let mut sender = String::from("Unknown sender"); // Messages for event in room.messages.messages.iter() { #[allow(clippy::single_match)] match event { AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(message)) => { - let sender = - match block_on(async { joined.get_member(&message.sender).await }) { + // Display sender if message is from new sender + if last_sender.as_ref() != Some(&message.sender) { + last_sender = Some(message.sender.clone()); + sender = match block_on(async { + joined.get_member(&message.sender).await + }) { Some(member) => member.name().to_owned(), None => message.sender.to_string(), }; + scroll = scroll + .push(iced::Space::with_height(4.into())) + .push(Text::new(&sender).color([0.0, 0.0, 1.0])); + } let content: Element<_> = match &message.content { MessageEventContent::Audio(audio) => { Text::new(format!("Audio message: {}", audio.body)) @@ -380,7 +496,7 @@ impl MainView { if let Some(ref url) = image.url { match self.images.get(url) { Some(handle) => Container::new( - iced::Image::new(handle.to_owned()) + Image::new(handle.to_owned()) .width(800.into()) .height(1200.into()), ) @@ -414,13 +530,15 @@ impl MainView { }; let row = Row::new() .spacing(5) - .push(Text::new(sender).color([0.0, 0.0, 1.0])) .push(content) .push(Text::new(format_systime(message.origin_server_ts))); scroll = scroll.push(row); } AnyRoomEvent::Message(AnyMessageEvent::RoomEncrypted(_encrypted)) => { - scroll = scroll.push(Text::new("Encrypted event").color([0.3, 0.3, 0.3])) + scroll = scroll.push(Text::new("Encrypted event").color([0.3, 0.3, 0.3])); + } + AnyRoomEvent::RedactedMessage(_) => { + scroll = scroll.push(Text::new("Deleted message").color([0.3, 0.3, 0.3])); } _ => (), } @@ -468,21 +586,24 @@ impl MainView { Some(emojis) => { let mut row = Row::new().push(Text::new("Verify emojis match:")); for (emoji, name) in emojis.iter() { - row = row.push( - Column::new() - .align_items(iced::Align::Center) - .push(Text::new(*emoji).size(32)) - .push(Text::new(*name)), - ); + row = row + .push( + Column::new() + .align_items(iced::Align::Center) + .push(Text::new(*emoji).size(32)) + .push(Text::new(*name)), + ) + .spacing(5); } - row.push( - Button::new(&mut self.sas_accept_button, Text::new("Confirm")) - .on_press(Message::VerificationConfirm), - ) - .push( - Button::new(&mut self.sas_deny_button, Text::new("Deny")) - .on_press(Message::VerificationCancel), - ) + row.push(iced::Space::with_width(Length::Fill)) + .push( + Button::new(&mut self.sas_accept_button, Text::new("Confirm")) + .on_press(Message::VerificationConfirm), + ) + .push( + Button::new(&mut self.sas_deny_button, Text::new("Deny")) + .on_press(Message::VerificationCancel), + ) } None => Row::new() .push( @@ -564,9 +685,10 @@ pub enum Message { // Main state messages /// Reset state for room ResetRoom(RoomId, RoomEntry), + RoomName(RoomId, String), /// Get backfill for given room BackFill(RoomId), - /// Received backfille + /// Received backfill BackFilled(RoomId, MessageResponse), /// Fetch an image pointed to by an mxc url FetchImage(String), @@ -719,24 +841,16 @@ impl Application for Retrix { *self = Retrix::LoggedIn(MainView::new(client.clone(), session)); let mut commands: Vec> = Vec::new(); for room in client.joined_rooms().into_iter() { - //let client = client.clone(); + let room = std::sync::Arc::new(room); + let r = room.clone(); let command: Command<_> = async move { - let room = room.clone(); - let entry = RoomEntry::from_sdk(&room); - - // Display name calculation for DMs is bronk so we're doing it - // ourselves - /*if let Some(ref direct) = entry.direct { - let request = DisplayNameRequest::new(direct); - if let Ok(response) = client.send(request).await { - if let Some(name) = response.displayname { - entry.name = name; - } - } - }*/ - Message::ResetRoom(room.room_id().to_owned(), entry) + let entry = RoomEntry::from_sdk(&r).await; + Message::ResetRoom(r.room_id().to_owned(), entry) } .into(); + if let Some(url) = room.avatar_url() { + commands.push(async { Message::FetchImage(url) }.into()) + } commands.push(command); } return Command::batch(commands); @@ -807,45 +921,67 @@ impl Application for Retrix { room.messages.push(AnyRoomEvent::State(event)); } AnyStateEvent::RoomName(ref name) => { - let room = view.rooms.entry(name.room_id.clone()).or_default(); + let id = name.room_id.clone(); + let room = view.rooms.entry(id.clone()).or_default(); room.display_name = name.content.name().map(String::from); - if let Some(joined) = view.client.get_joined_room(&name.room_id) { - room.name = block_on(async { joined.display_name().await }); - } room.messages.push(AnyRoomEvent::State(event)); + let client = view.client.clone(); + return async move { + let joined = client.get_joined_room(&id).unwrap(); + Message::RoomName(id, joined.display_name().await) + } + .into(); } AnyStateEvent::RoomTopic(ref topic) => { let room = view.rooms.entry(topic.room_id.clone()).or_default(); room.topic = topic.content.topic.clone(); room.messages.push(AnyRoomEvent::State(event)); } + AnyStateEvent::RoomAvatar(ref avatar) => { + let room = view.rooms.entry(avatar.room_id.clone()).or_default(); + room.messages.push(AnyRoomEvent::State(event)); + if let Some(url) = room.avatar.clone() { + room.avatar = Some(url.clone()); + return async { Message::FetchImage(url) }.into(); + } + } AnyStateEvent::RoomCreate(ref create) => { // Add room to the entry list - let room = match view.client.get_joined_room(&create.room_id) { - Some(joined) => view - .rooms - .entry(create.room_id.clone()) - .or_insert_with(|| RoomEntry::from_sdk(&joined)), - None => view.rooms.entry(create.room_id.clone()).or_default(), - }; - room.messages.push(AnyRoomEvent::State(event)); + let joined = view.client.get_joined_room(&create.room_id).unwrap(); + let id = create.room_id.clone(); + return async move { + let mut entry = RoomEntry::from_sdk(&joined).await; + entry.messages.push(AnyRoomEvent::State(event)); + Message::ResetRoom(id, entry) + } + .into(); } AnyStateEvent::RoomMember(ref member) => { let room = view.rooms.entry(member.room_id.clone()).or_default(); let client = view.client.clone(); // If we left a room, remove it from the RoomEntry list - let own_id = block_on(async { client.user_id().await }) - .unwrap() - .to_string(); - if member.content.membership == MembershipState::Leave - && member.state_key == own_id - { - // Deselect room if we're leaving selected room - if view.selected.as_ref() == Some(&member.room_id) { - view.selected = None; + if member.state_key == view.session.user_id { + match member.content.membership { + MembershipState::Join => { + let id = member.room_id.clone(); + return async move { + let joined = client.get_joined_room(&id).unwrap(); + let entry = RoomEntry::from_sdk(&joined).await; + + Message::ResetRoom(id, entry) + } + .into(); + } + MembershipState::Leave => { + // Deselect room if we're leaving selected room + if view.selected.as_ref() == Some(&member.room_id) { + view.selected = None; + } + view.rooms.remove(&member.room_id); + return Command::none(); + } + _ => (), } - view.rooms.remove(&member.room_id); - return Command::none(); } room.messages.push(AnyRoomEvent::State(event)); } @@ -855,14 +991,20 @@ impl Application for Retrix { room.messages.push(AnyRoomEvent::State(event)); } }, - _ => (), + AnyRoomEvent::RedactedMessage(redacted) => { + let room = view.rooms.entry(redacted.room_id().clone()).or_default(); + room.messages.push(AnyRoomEvent::RedactedMessage(redacted)); + } + AnyRoomEvent::RedactedState(redacted) => { + let room = view.rooms.entry(redacted.room_id().clone()).or_default(); + room.messages.push(AnyRoomEvent::RedactedState(redacted)); + } }, matrix::Event::ToDevice(event) => match event { AnyToDeviceEvent::KeyVerificationStart(start) => { let client = view.client.clone(); return Command::perform( async move { - //tokio::time::delay_for(std::time::Duration::from_secs(2)).await; client.get_verification(&start.content.transaction_id).await }, Message::SetVerification, @@ -891,7 +1033,8 @@ impl Application for Retrix { .unwrap_or_else(|| view.sync_token.clone()), }; return async move { - let request = MessageRequest::backward(&id, &token); + let mut request = MessageRequest::backward(&id, &token); + request.limit = matrix_sdk::uint!(30); match client.room_messages(request).await { Ok(response) => Message::BackFilled(id, response), Err(e) => Message::ErrorMessage(e.to_string()), @@ -935,10 +1078,6 @@ impl Application for Retrix { return async move { Message::ErrorMessage(e.to_string()) }.into() } }; - println!( - "Getting '{}' from '{}', with url '{}'", - &server, &path, &url - ); let client = view.client.clone(); return async move { let request = ImageRequest::new(&path, &*server); @@ -956,6 +1095,11 @@ impl Application for Retrix { Message::FetchedImage(url, handle) => { view.images.insert(url, handle); } + Message::RoomName(id, name) => { + if let Some(room) = view.rooms.get_mut(&id) { + room.name = name; + } + } Message::SetVerification(v) => view.sas = v, Message::VerificationAccept => { let sas = match &view.sas {