diff --git a/Cargo.toml b/Cargo.toml index affd0c7..4947e18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,20 +12,21 @@ 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"] } +iced = { git = "https://github.com/hecrj/iced", rev = "90fee3a", features = ["debug", "image", "tokio"] } +iced_futures = { git = "https://github.com/hecrj/iced", rev = "90fee3a" } +#iced = { git = "https://github.com/hecrj/iced", rev = "90fee3a", features = ["debug", "image", "tokio", "glow"] } +#iced_glow = { git = "https://github.com/hecrj/iced", rev = "90fee3a", features = ["image"] } serde = { version = "1.0", features = ["derive"] } time = "0.2" -tokio = { version = "1.0", features = ["sync"] } +tokio = { version = "1.1", features = ["sync"] } toml = "0.5" tracing-subscriber = { version = "0.2", features = ["parking_lot"] } [dependencies.matrix-sdk] git = "https://github.com/matrix-org/matrix-rust-sdk" -rev = "40c53f0" +rev = "ff68360" default_features = false -features = ["encryption", "sqlite_cryptostore", "messages", "rustls-tls", "unstable-synapse-quirks"] +features = ["encryption", "rustls-tls", "unstable-synapse-quirks", "sled_cryptostore"] [profile.release] lto = "thin" diff --git a/README.md b/README.md index 3c3d3d7..b6e6737 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 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 +## Features - [x] Rooms - [x] List rooms - [ ] Join rooms @@ -15,7 +15,7 @@ The project is currently in early stages, and is decidedly not feature complete. - [x] Plain text - [ ] Formatted text (waiting on iced, markdown will be shown raw) - [ ] Stickers - - [ ] Images + - [x] Images (in unencrypted rooms) - [ ] Audio - [ ] Video - [ ] Location @@ -30,8 +30,18 @@ The project is currently in early stages, and is decidedly not feature complete. - [x] Display name - [ ] Avatar -## Things I (currently) don't intend to implement +### Things I (currently) don't intend to implement - VoIP Calls +## Building +Retrix can be compiled with +```bash +cargo build --release +``` +Be warned that retrix is very heavy to build due to the dependencies it uses. On the less powerful of my laptops, it takes on average 6 minutes to build in release mode. + +## Installing +You can put the compiled binary wherever binaries go. Retrix keeps its configuration and caching data in `~/.config/retrix` on linux systems, and in `%APPDATA%\retrix` on windows systems. It will automatically create the needed folder if it does not exist. + [iced]: https://github.com/hecrj/iced [matrix-rust-sdk]: https://github.com/matrix-org/matrix-rust-sdk diff --git a/src/matrix.rs b/src/matrix.rs index 3ee3ade..eddcf4d 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -1,5 +1,6 @@ use std::{ convert::TryFrom, + sync::Arc, time::{Duration, SystemTime}, }; @@ -7,10 +8,10 @@ use async_stream::stream; use matrix_sdk::{ api::r0::{account::register::Request as RegistrationRequest, uiaa::AuthData}, events::{ - room::message::{MessageEvent, MessageEventContent}, + room::message::{MessageEvent, MessageEventContent, MessageType}, AnyMessageEvent, AnyRoomEvent, AnySyncRoomEvent, AnyToDeviceEvent, }, - identifiers::{DeviceId, EventId, ServerName, UserId}, + identifiers::{DeviceId, EventId, RoomId, ServerName, UserId}, reqwest::Url, Client, ClientConfig, LoopCtrl, SyncSettings, }; @@ -171,6 +172,7 @@ pub fn parse_mxc(url: &str) -> Result<(Box, String), Error> { } } +/// Makes an iced subscription for listening for matrix events. pub struct MatrixSync { client: matrix_sdk::Client, join: Option>, @@ -183,52 +185,21 @@ impl MatrixSync { } } +/// A matrix event that should be passed to the iced subscription #[derive(Clone, Debug)] pub enum Event { - Room(AnyRoomEvent), + /// An event for an invited room + Invited(AnyRoomEvent, Arc), + /// An event for a joined room + Joined(AnyRoomEvent, Arc), + /// An event for a left room + Left(AnyRoomEvent, Arc), + /// A to-device event ToDevice(AnyToDeviceEvent), + /// Synchronization token 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, @@ -255,33 +226,16 @@ where .token(client.sync_token().await.unwrap()) .timeout(Duration::from_secs(30)), |response| async { - //sender.send(Event::Token(response.next_batch)).ok(); for (id, room) in response.rooms.join { + let joined = Arc::new(client.get_joined_room(&id).unwrap()); for event in room.state.events { let id = id.clone(); - sender - .send(Event::Room(AnyRoomEvent::State( - event.into_full_event(id), - ))) - .ok(); + let event = AnyRoomEvent::State(event.into_full_event(id)); + sender.send(Event::Joined(event, Arc::clone(&joined))).ok(); } for event in room.timeline.events { - let id = id.clone(); - let event = match event { - AnySyncRoomEvent::Message(e) => { - AnyRoomEvent::Message(e.into_full_event(id)) - } - AnySyncRoomEvent::State(e) => { - AnyRoomEvent::State(e.into_full_event(id)) - } - AnySyncRoomEvent::RedactedMessage(e) => { - AnyRoomEvent::RedactedMessage(e.into_full_event(id)) - } - AnySyncRoomEvent::RedactedState(e) => { - AnyRoomEvent::RedactedState(e.into_full_event(id)) - } - }; - sender.send(Event::Room(event)).ok(); + let event = event.into_full_event(id.clone()); + sender.send(Event::Joined(event, Arc::clone(&joined))).ok(); } } for event in response.to_device.events { @@ -345,10 +299,33 @@ impl AnyMessageEventExt for AnyMessageEvent { fn image_url(&self) -> Option { match self { AnyMessageEvent::RoomMessage(MessageEvent { - content: MessageEventContent::Image(ref image), + content: + MessageEventContent { + msgtype: MessageType::Image(ref image), + .. + }, .. }) => image.url.clone(), _ => None, } } } + +pub trait AnySyncRoomEventExt { + fn into_full_event(self, room_id: RoomId) -> AnyRoomEvent; +} + +impl AnySyncRoomEventExt for AnySyncRoomEvent { + fn into_full_event(self, id: RoomId) -> AnyRoomEvent { + match self { + AnySyncRoomEvent::Message(e) => AnyRoomEvent::Message(e.into_full_event(id)), + AnySyncRoomEvent::State(e) => AnyRoomEvent::State(e.into_full_event(id)), + AnySyncRoomEvent::RedactedMessage(e) => { + AnyRoomEvent::RedactedMessage(e.into_full_event(id)) + } + AnySyncRoomEvent::RedactedState(e) => { + AnyRoomEvent::RedactedState(e.into_full_event(id)) + } + } + } +} diff --git a/src/ui.rs b/src/ui.rs index bf10b32..04d2280 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -17,7 +17,7 @@ use matrix_sdk::{ key::verification::cancel::CancelCode as VerificationCancelCode, room::{ member::MembershipState, - message::{MessageEventContent, Relation, TextMessageEventContent}, + message::{MessageEventContent, MessageType, Relation}, }, AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent, AnyStateEvent, AnyToDeviceEvent, }, @@ -28,10 +28,13 @@ use crate::matrix::{self, AnyMessageEventExt, AnyRoomEventExt}; pub mod prompt; pub mod settings; +pub mod theme; use prompt::{PromptAction, PromptView}; use settings::SettingsView; +const THUMBNAIL_SIZE: u32 = 48; + /// What order to sort rooms in in the room list. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RoomSorting { @@ -59,10 +62,10 @@ pub struct RoomEntry { } impl RoomEntry { - pub async fn from_sdk(room: &matrix_sdk::JoinedRoom) -> Self { + pub async fn from_sdk(room: &matrix_sdk::room::Joined) -> Self { Self { direct: room.direct_target(), - name: room.display_name().await, + name: room.display_name().await.unwrap(), topic: room.topic().unwrap_or_default(), alias: room.canonical_alias(), avatar: room.avatar_url(), @@ -116,10 +119,10 @@ impl MessageBuffer { if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage( matrix_sdk::events::MessageEvent { content: - MessageEventContent::Text(TextMessageEventContent { + MessageEventContent { relates_to: Some(Relation::Replacement(ref replacement)), .. - }), + }, .. }, )) = event @@ -142,10 +145,10 @@ impl MessageBuffer { if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage( matrix_sdk::events::MessageEvent { content: - MessageEventContent::Text(TextMessageEventContent { + MessageEventContent { relates_to: Some(Relation::Replacement(replacement)), .. - }), + }, .. }, )) = event @@ -207,6 +210,8 @@ pub struct MainView { rooms: BTreeMap, /// A map of mxc urls to image data images: BTreeMap, + /// A map of mxc urls to image thumbnails + thumbnails: BTreeMap, /// Room list entries for direct conversations dm_buttons: Vec, @@ -245,6 +250,7 @@ impl MainView { rooms: Default::default(), selected: None, images: Default::default(), + thumbnails: Default::default(), room_scroll: Default::default(), message_scroll: Default::default(), backfill_button: Default::default(), @@ -298,7 +304,7 @@ impl MainView { Vec<(&RoomId, &RoomEntry)>, ) = rooms .iter() - // Hide if we're in the room the tombstone points to + // Hide if have joined the room the tombstone points to .filter(|(id, _)| { !client .get_joined_room(id) @@ -324,6 +330,7 @@ impl MainView { self.group_buttons .resize_with(group_rooms.len(), Default::default); // Create buttons + let thumbnails = &self.thumbnails; let images = &self.images; let dm_buttons: Vec> = self .dm_buttons @@ -333,13 +340,13 @@ impl MainView { // TODO: highlight selected let (id, room) = unsafe { dm_rooms.get_unchecked(idx) }; let name = if room.name.is_empty() { - "Missing name" + "Empty room" } 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) { + if let Some(handle) = thumbnails.get(url) { row = row.push( Image::new(handle.clone()) .width(20.into()) @@ -467,32 +474,30 @@ impl MainView { sender = match block_on(async { joined.get_member(&message.sender).await }) { - Some(member) => member.name().to_owned(), - None => message.sender.to_string(), + Ok(Some(member)) => member.name().to_owned(), + _ => 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) => { + let content: Element<_> = match &message.content.msgtype { + MessageType::Audio(audio) => { Text::new(format!("Audio message: {}", audio.body)) .color([0.2, 0.2, 0.2]) .width(Length::Fill) .into() } - MessageEventContent::Emote(emote) => { + MessageType::Emote(emote) => { Text::new(format!("* {} {}", sender, emote.body)) .width(Length::Fill) .into() } - MessageEventContent::File(file) => { - Text::new(format!("File '{}'", file.body)) - .color([0.2, 0.2, 0.2]) - .width(Length::Fill) - .into() - } - MessageEventContent::Image(image) => { + MessageType::File(file) => Text::new(format!("File '{}'", file.body)) + .color([0.2, 0.2, 0.2]) + .width(Length::Fill) + .into(), + MessageType::Image(image) => { if let Some(ref url) = image.url { match self.images.get(url) { Some(handle) => Container::new( @@ -512,16 +517,16 @@ impl MainView { .into() } } - MessageEventContent::Notice(notice) => { + MessageType::Notice(notice) => { Text::new(¬ice.body).width(Length::Fill).into() } - MessageEventContent::ServerNotice(notice) => { + MessageType::ServerNotice(notice) => { Text::new(¬ice.body).width(Length::Fill).into() } - MessageEventContent::Text(text) => { + MessageType::Text(text) => { Text::new(&text.body).width(Length::Fill).into() } - MessageEventContent::Video(video) => { + MessageType::Video(video) => { Text::new(format!("Video: {}", video.body)) .color([0.2, 0.2, 0.2]) .into() @@ -658,6 +663,389 @@ impl MainView { root_row.into() } + + fn update(&mut self, message: Message) -> Command { + let view = self; + match message { + Message::ErrorMessage(e) => view.error = Some((e, Default::default())), + Message::ClearError => view.error = None, + Message::SetSort(s) => view.sorting = s, + Message::ResetRoom(id, room) => { + view.rooms.insert(id.clone(), room); + return async move { Message::BackFill(id) }.into(); + } + Message::SelectRoom(r) => { + view.selected = Some(r.clone()); + if view.rooms.get(&r).unwrap().messages.messages.is_empty() { + return async move { Message::BackFill(r) }.into(); + } + } + Message::Sync(event) => match event { + matrix::Event::Joined(event, joined) => match event { + AnyRoomEvent::Message(event) => { + let room = view.rooms.entry(event.room_id().clone()).or_default(); + room.messages.push(AnyRoomEvent::Message(event.clone())); + let mut commands = Vec::new(); + // Add fetch image command if the message has an image + let img_cmd = match event.image_url() { + Some(url) => async { Message::FetchImage(url) }.into(), + None => Command::none(), + }; + commands.push(img_cmd); + // Set read marker if message is in selected room + /*if view.selected.as_ref() == Some(event.room_id()) { + let client = view.client.clone(); + let marker_cmd = async move { + let result = client + .read_marker( + event.room_id(), + event.event_id(), + Some(event.event_id()), + ) + .await + .err(); + match result { + Some(err) => Message::ErrorMessage(err.to_string()), + // TODO: Make this an actual no-op + None => Message::Login, + } + } + .into(); + commands.push(marker_cmd); + }*/ + + return Command::batch(commands); + } + AnyRoomEvent::State(event) => match event { + AnyStateEvent::RoomCanonicalAlias(ref alias) => { + let room = view.rooms.entry(alias.room_id.clone()).or_default(); + room.alias = alias.content.alias.clone(); + room.messages.push(AnyRoomEvent::State(event)); + } + AnyStateEvent::RoomName(ref name) => { + let id = name.room_id.clone(); + let room = view.rooms.entry(id.clone()).or_default(); + room.display_name = name.content.name().map(String::from); + room.messages.push(AnyRoomEvent::State(event)); + let client = view.client.clone(); + return async move { + let joined = client.get_joined_room(&id).unwrap(); + match joined.display_name().await { + Ok(name) => Message::RoomName(id, name), + Err(e) => Message::ErrorMessage(e.to_string()), + } + } + .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::FetchThumb(url) }.into(); + return Command::none(); + } + } + AnyStateEvent::RoomCreate(ref create) => { + // Add room to the entry list + 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 + 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(); + } + _ => (), + } + } + room.messages.push(AnyRoomEvent::State(event)); + } + ref any => { + // Ensure room exists + let room = view.rooms.entry(any.room_id().clone()).or_default(); + 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 { client.get_verification(&start.content.transaction_id).await }, + Message::SetVerification, + ); + } + AnyToDeviceEvent::KeyVerificationCancel(cancel) => { + return async { Message::VerificationCancelled(cancel.content.code) } + .into(); + } + _ => (), + }, + matrix::Event::Token(token) => { + view.sync_token = token; + } + _ => (), + }, + Message::BackFill(id) => { + let entry = view.rooms.entry(id.clone()).or_default(); + entry.messages.loading = true; + let client = view.client.clone(); + let room = client.get_joined_room(&id).unwrap(); + let token = match entry.messages.end.clone() { + Some(end) => end, + None => room + .last_prev_batch() + .unwrap_or_else(|| view.sync_token.clone()), + }; + return async move { + let mut request = MessageRequest::backward(&id, &token); + request.limit = matrix_sdk::uint!(30); + match room.messages(request).await { + Ok(response) => Message::BackFilled(id, response), + Err(e) => Message::ErrorMessage(e.to_string()), + } + } + .into(); + } + Message::BackFilled(id, response) => { + let room = view.rooms.get_mut(&id).unwrap(); + room.messages.loading = false; + let events: Vec = response + .chunk + .into_iter() + .filter_map(|e| e.deserialize().ok()) + .chain( + response + .state + .into_iter() + .filter_map(|e| e.deserialize().ok().map(AnyRoomEvent::State)), + ) + .collect(); + + if let Some(start) = response.start { + room.messages.start = Some(start); + } + if let Some(end) = response.end { + room.messages.end = Some(end); + } + let commands: Vec> = events + .iter() + .filter_map(|e| e.image_url()) + .map(|url| async { Message::FetchImage(url) }.into()) + .collect(); + room.messages.append(events); + return Command::batch(commands); + } + + Message::FetchImage(url) => { + let (server, path) = match matrix::parse_mxc(&url) { + Ok((server, path)) => (server, path), + Err(e) => return async move { Message::ErrorMessage(e.to_string()) }.into(), + }; + let client = view.client.clone(); + return async move { + let request = ImageRequest::new(&path, &*server); + let response = client.send(request, None).await; + match response { + Ok(response) => Message::FetchedImage( + url, + iced::image::Handle::from_memory(response.file), + ), + Err(e) => Message::ErrorMessage(e.to_string()), + } + } + .into(); + } + Message::FetchedImage(url, handle) => { + view.images.insert(url, handle); + } + Message::FetchedThumbnail(url, handle) => { + view.thumbnails.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 { + Some(sas) => sas.clone(), + None => return Command::none(), + }; + return Command::perform( + async move { sas.accept().await }, + |result| match result { + Ok(()) => Message::VerificationAccepted, + Err(e) => Message::ErrorMessage(e.to_string()), + }, + ); + } + Message::VerificationConfirm => { + let sas = match &view.sas { + Some(sas) => sas.clone(), + None => return Command::none(), + }; + return Command::perform( + async move { sas.confirm().await }, + |result| match result { + Ok(()) => Message::VerificationConfirmed, + Err(e) => Message::ErrorMessage(e.to_string()), + }, + ); + } + Message::VerificationCancel => { + let sas = match &view.sas { + Some(sas) => sas.clone(), + None => return Command::none(), + }; + return Command::perform( + async move { sas.cancel().await }, + |result| match result { + Ok(()) => Message::VerificationCancelled(VerificationCancelCode::User), + Err(e) => Message::ErrorMessage(e.to_string()), + }, + ); + } + Message::VerificationCancelled(code) => { + view.sas = None; + return async move { Message::ErrorMessage(code.as_str().to_owned()) }.into(); + } + Message::VerificationClose => view.sas = None, + Message::SetMessage(m) => view.draft = m, + Message::SendMessage => { + let selected = match view.selected.clone() { + Some(selected) => selected, + None => return Command::none(), + }; + let draft = view.draft.clone(); + let client = view.client.clone(); + return Command::perform( + async move { + client + .room_send( + &selected, + AnyMessageEventContent::RoomMessage( + MessageEventContent::text_plain(draft), + ), + None, + ) + .await + }, + |result| match result { + Ok(_) => Message::SetMessage(String::new()), + Err(e) => Message::ErrorMessage(e.to_string()), + }, + ); + } + Message::OpenSettings => { + view.settings_view = Some(SettingsView::new()); + let client = view.client.clone(); + return Command::perform( + async move { + client + .display_name() + .await + .unwrap_or_default() + .unwrap_or_default() + }, + Message::SetDisplayNameInput, + ); + } + Message::SetDisplayNameInput(name) => { + if let Some(ref mut settings) = view.settings_view { + settings.display_name = name; + } + } + Message::SaveDisplayName => { + if let Some(ref mut settings) = view.settings_view { + let client = view.client.clone(); + let name = settings.display_name.clone(); + settings.saving_name = true; + return Command::perform( + async move { client.set_display_name(Some(&name)).await }, + |result| match result { + Ok(()) => Message::DisplayNameSaved, + // TODO: set saving to false and report error + Err(_) => Message::DisplayNameSaved, + }, + ); + } + } + Message::DisplayNameSaved => { + if let Some(ref mut settings) = view.settings_view { + settings.saving_name = false; + } + } + Message::SetKeyPath(p) => { + if let Some(ref mut settings) = view.settings_view { + settings.key_path = p; + } + } + Message::SetKeyPassword(p) => { + if let Some(ref mut settings) = view.settings_view { + settings.key_password = p; + } + } + Message::ImportKeys => { + if let Some(ref settings) = view.settings_view { + let path = std::path::PathBuf::from(&settings.key_path); + let password = settings.key_password.clone(); + let client = view.client.clone(); + return Command::perform( + async move { client.import_keys(path, &password).await }, + |result| match result { + Ok(_) => Message::SetKeyPassword(String::new()), + // TODO: Actual error reporting here + Err(e) => Message::SetKeyPath(e.to_string()), + }, + ); + } + } + Message::CloseSettings => view.settings_view = None, + _ => (), + }; + Command::none() + } } #[allow(clippy::large_enum_variant)] @@ -670,6 +1058,8 @@ pub enum Retrix { #[derive(Debug, Clone)] pub enum Message { + /// Do nothing + Noop, // Login form messages SetUser(String), SetPassword(String), @@ -690,6 +1080,8 @@ pub enum Message { BackFill(RoomId), /// Received backfill BackFilled(RoomId, MessageResponse), + /// Fetched a thumbnail + FetchedThumbnail(String, iced::image::Handle), /// Fetch an image pointed to by an mxc url FetchImage(String), /// Fetched an image @@ -781,7 +1173,11 @@ impl Application for Retrix { } } - fn update(&mut self, message: Self::Message) -> Command { + fn update( + &mut self, + message: Self::Message, + _clipboard: &mut iced::Clipboard, + ) -> Command { match self { Retrix::Prompt(prompt) => match message { Message::SetUser(u) => prompt.user = u, @@ -848,395 +1244,30 @@ impl Application for Retrix { 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); - } - _ => (), - }, - Retrix::LoggedIn(view) => match message { - Message::ErrorMessage(e) => view.error = Some((e, Default::default())), - Message::ClearError => view.error = None, - Message::SetSort(s) => view.sorting = s, - Message::ResetRoom(id, room) => { - view.rooms.insert(id.clone(), room); - return async move { Message::BackFill(id) }.into(); - } - Message::SelectRoom(r) => { - view.selected = Some(r.clone()); - if view.rooms.get(&r).unwrap().messages.messages.is_empty() { - return async move { Message::BackFill(r) }.into(); - } - } - Message::Sync(event) => match event { - matrix::Event::Room(event) => match event { - AnyRoomEvent::Message(event) => { - let room = view.rooms.entry(event.room_id().clone()).or_default(); - room.messages.push(AnyRoomEvent::Message(event.clone())); - let mut commands = Vec::new(); - let img_cmd = match event.image_url() { - Some(url) => async { Message::FetchImage(url) }.into(), - None => Command::none(), - }; - commands.push(img_cmd); - // Set read marker if message is in selected room - if view.selected.as_ref() == Some(event.room_id()) { - // Super duper gross ugly scroll to bottom hack - /*view.message_scroll = unsafe { - let mut tmp = std::mem::transmute::<_, (Option, f32)>( - view.message_scroll, - ); - tmp.1 = 999999.0; - std::mem::transmute::<_, iced::scrollable::State>(tmp) - };*/ - - let client = view.client.clone(); - let marker_cmd = async move { - let result = client - .read_marker( - event.room_id(), - event.event_id(), - Some(event.event_id()), - ) - .await - .err(); - match result { - Some(err) => Message::ErrorMessage(err.to_string()), - // TODO: Make this an actual no-op - None => Message::Login, - } - } - .into(); - commands.push(marker_cmd); - } - return Command::batch(commands); - } - AnyRoomEvent::State(event) => match event { - AnyStateEvent::RoomCanonicalAlias(ref alias) => { - let room = view.rooms.entry(alias.room_id.clone()).or_default(); - room.alias = alias.content.alias.clone(); - room.messages.push(AnyRoomEvent::State(event)); - } - AnyStateEvent::RoomName(ref name) => { - let id = name.room_id.clone(); - let room = view.rooms.entry(id.clone()).or_default(); - room.display_name = name.content.name().map(String::from); - 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 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 - 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(); - } - _ => (), - } - } - room.messages.push(AnyRoomEvent::State(event)); - } - ref any => { - // Ensure room exists - let room = view.rooms.entry(any.room_id().clone()).or_default(); - 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 { - client.get_verification(&start.content.transaction_id).await - }, - Message::SetVerification, - ); - } - AnyToDeviceEvent::KeyVerificationCancel(cancel) => { - return async { Message::VerificationCancelled(cancel.content.code) } - .into(); - } - _ => (), - }, - matrix::Event::Token(token) => { - view.sync_token = token; - } - }, - Message::BackFill(id) => { - let room = view.rooms.entry(id.clone()).or_default(); - room.messages.loading = true; - let client = view.client.clone(); - let token = match room.messages.end.clone() { - Some(end) => end, - None => client - .get_joined_room(&id) - .unwrap() - .last_prev_batch() - .unwrap_or_else(|| view.sync_token.clone()), - }; - return async move { - 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()), - } - } - .into(); - } - Message::BackFilled(id, response) => { - let room = view.rooms.get_mut(&id).unwrap(); - room.messages.loading = false; - let events: Vec = response - .chunk - .into_iter() - .filter_map(|e| e.deserialize().ok()) - .chain( - response - .state - .into_iter() - .filter_map(|e| e.deserialize().ok().map(AnyRoomEvent::State)), - ) - .collect(); - - if let Some(start) = response.start { - room.messages.start = Some(start); - } - if let Some(end) = response.end { - room.messages.end = Some(end); - } - let commands: Vec> = events - .iter() - .filter_map(|e| e.image_url()) - .map(|url| async { Message::FetchImage(url) }.into()) - .collect(); - room.messages.append(events); - return Command::batch(commands); - } - Message::FetchImage(url) => { - let (server, path) = match matrix::parse_mxc(&url) { - Ok((server, path)) => (server, path), - Err(e) => { - return async move { Message::ErrorMessage(e.to_string()) }.into() - } - }; - let client = view.client.clone(); - return async move { - let request = ImageRequest::new(&path, &*server); - let response = client.send(request).await; - match response { - Ok(response) => Message::FetchedImage( - url, - iced::image::Handle::from_memory(response.file), - ), - Err(e) => Message::ErrorMessage(e.to_string()), - } - } - .into(); - } - 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 { - Some(sas) => sas.clone(), - None => return Command::none(), - }; - return Command::perform( - async move { sas.accept().await }, - |result| match result { - Ok(()) => Message::VerificationAccepted, - Err(e) => Message::ErrorMessage(e.to_string()), - }, - ); - } - Message::VerificationConfirm => { - let sas = match &view.sas { - Some(sas) => sas.clone(), - None => return Command::none(), - }; - return Command::perform(async move { sas.confirm().await }, |result| { - match result { - Ok(()) => Message::VerificationConfirmed, - Err(e) => Message::ErrorMessage(e.to_string()), - } - }); - } - Message::VerificationCancel => { - let sas = match &view.sas { - Some(sas) => sas.clone(), - None => return Command::none(), - }; - return Command::perform( - async move { sas.cancel().await }, - |result| match result { - Ok(()) => Message::VerificationCancelled(VerificationCancelCode::User), - Err(e) => Message::ErrorMessage(e.to_string()), - }, - ); - } - Message::VerificationCancelled(code) => { - view.sas = None; - return async move { Message::ErrorMessage(code.as_str().to_owned()) }.into(); - } - Message::VerificationClose => view.sas = None, - Message::SetMessage(m) => view.draft = m, - Message::SendMessage => { - let selected = match view.selected.clone() { - Some(selected) => selected, - None => return Command::none(), - }; - let draft = view.draft.clone(); - let client = view.client.clone(); - return Command::perform( - async move { - client - .room_send( - &selected, - AnyMessageEventContent::RoomMessage( - MessageEventContent::text_plain(draft), + // Fetch room avatar thumbnail if available + commands.push( + async move { + match room + .avatar(Some(THUMBNAIL_SIZE), Some(THUMBNAIL_SIZE)) + .await + { + Ok(Some(avatar)) => Message::FetchedThumbnail( + room.avatar_url().unwrap(), + iced::image::Handle::from_memory(avatar), ), - None, - ) - .await - }, - |result| match result { - Ok(_) => Message::SetMessage(String::new()), - Err(e) => Message::ErrorMessage(e.to_string()), - }, - ); - } - Message::OpenSettings => { - view.settings_view = Some(SettingsView::new()); - let client = view.client.clone(); - return Command::perform( - async move { - client - .display_name() - .await - .unwrap_or_default() - .unwrap_or_default() - }, - Message::SetDisplayNameInput, - ); - } - Message::SetDisplayNameInput(name) => { - if let Some(ref mut settings) = view.settings_view { - settings.display_name = name; + Ok(None) => Message::Noop, + Err(e) => Message::ErrorMessage(e.to_string()), + } + } + .into(), + ) } + return Command::batch(commands); } - Message::SaveDisplayName => { - if let Some(ref mut settings) = view.settings_view { - let client = view.client.clone(); - let name = settings.display_name.clone(); - settings.saving_name = true; - return Command::perform( - async move { client.set_display_name(Some(&name)).await }, - |result| match result { - Ok(()) => Message::DisplayNameSaved, - // TODO: set saving to false and report error - Err(_) => Message::DisplayNameSaved, - }, - ); - } - } - Message::DisplayNameSaved => { - if let Some(ref mut settings) = view.settings_view { - settings.saving_name = false; - } - } - Message::SetKeyPath(p) => { - if let Some(ref mut settings) = view.settings_view { - settings.key_path = p; - } - } - Message::SetKeyPassword(p) => { - if let Some(ref mut settings) = view.settings_view { - settings.key_password = p; - } - } - Message::ImportKeys => { - if let Some(ref settings) = view.settings_view { - let path = std::path::PathBuf::from(&settings.key_path); - let password = settings.key_password.clone(); - let client = view.client.clone(); - return Command::perform( - async move { client.import_keys(path, &password).await }, - |result| match result { - Ok(_) => Message::SetKeyPassword(String::new()), - // TODO: Actual error reporting here - Err(e) => Message::SetKeyPath(e.to_string()), - }, - ); - } - } - Message::CloseSettings => view.settings_view = None, _ => (), }, + Retrix::LoggedIn(view) => return view.update(message), }; Command::none() } diff --git a/src/ui/theme.rs b/src/ui/theme.rs new file mode 100644 index 0000000..d9f0bf6 --- /dev/null +++ b/src/ui/theme.rs @@ -0,0 +1,6 @@ +//! Theming for widgets. + +/// Which colorscheme to use +pub enum Theme { + Default, +}