update dependencies, code restructuring, use thumbnails for rooms

main
Amanda Graven 2021-04-01 10:01:48 +02:00
parent a8b76eaded
commit fe3d384435
Signed by: amanda
GPG Key ID: 45C461CDC9286390
5 changed files with 509 additions and 484 deletions

View File

@ -12,20 +12,21 @@ async-stream = "0.3"
async-trait = "0.1" async-trait = "0.1"
dirs-next = "2.0" dirs-next = "2.0"
futures = "0.3" futures = "0.3"
iced = { git = "https://github.com/hecrj/iced", rev = "31522e3", features = ["debug", "image", "tokio"] } iced = { git = "https://github.com/hecrj/iced", rev = "90fee3a", features = ["debug", "image", "tokio"] }
iced_futures = { git = "https://github.com/hecrj/iced", rev = "31522e3" } iced_futures = { git = "https://github.com/hecrj/iced", rev = "90fee3a" }
#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", "glow"] }
#iced_glow = { git = "https://github.com/hecrj/iced", rev = "90fee3a", features = ["image"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
time = "0.2" time = "0.2"
tokio = { version = "1.0", features = ["sync"] } tokio = { version = "1.1", features = ["sync"] }
toml = "0.5" toml = "0.5"
tracing-subscriber = { version = "0.2", features = ["parking_lot"] } tracing-subscriber = { version = "0.2", features = ["parking_lot"] }
[dependencies.matrix-sdk] [dependencies.matrix-sdk]
git = "https://github.com/matrix-org/matrix-rust-sdk" git = "https://github.com/matrix-org/matrix-rust-sdk"
rev = "40c53f0" rev = "ff68360"
default_features = false default_features = false
features = ["encryption", "sqlite_cryptostore", "messages", "rustls-tls", "unstable-synapse-quirks"] features = ["encryption", "rustls-tls", "unstable-synapse-quirks", "sled_cryptostore"]
[profile.release] [profile.release]
lto = "thin" lto = "thin"

View File

@ -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. 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] Rooms
- [x] List rooms - [x] List rooms
- [ ] Join rooms - [ ] Join rooms
@ -15,7 +15,7 @@ The project is currently in early stages, and is decidedly not feature complete.
- [x] Plain text - [x] Plain text
- [ ] Formatted text (waiting on iced, markdown will be shown raw) - [ ] Formatted text (waiting on iced, markdown will be shown raw)
- [ ] Stickers - [ ] Stickers
- [ ] Images - [x] Images (in unencrypted rooms)
- [ ] Audio - [ ] Audio
- [ ] Video - [ ] Video
- [ ] Location - [ ] Location
@ -30,8 +30,18 @@ The project is currently in early stages, and is decidedly not feature complete.
- [x] Display name - [x] Display name
- [ ] Avatar - [ ] Avatar
## Things I (currently) don't intend to implement ### Things I (currently) don't intend to implement
- VoIP Calls - 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 [iced]: https://github.com/hecrj/iced
[matrix-rust-sdk]: https://github.com/matrix-org/matrix-rust-sdk [matrix-rust-sdk]: https://github.com/matrix-org/matrix-rust-sdk

View File

@ -1,5 +1,6 @@
use std::{ use std::{
convert::TryFrom, convert::TryFrom,
sync::Arc,
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
@ -7,10 +8,10 @@ use async_stream::stream;
use matrix_sdk::{ use matrix_sdk::{
api::r0::{account::register::Request as RegistrationRequest, uiaa::AuthData}, api::r0::{account::register::Request as RegistrationRequest, uiaa::AuthData},
events::{ events::{
room::message::{MessageEvent, MessageEventContent}, room::message::{MessageEvent, MessageEventContent, MessageType},
AnyMessageEvent, AnyRoomEvent, AnySyncRoomEvent, AnyToDeviceEvent, AnyMessageEvent, AnyRoomEvent, AnySyncRoomEvent, AnyToDeviceEvent,
}, },
identifiers::{DeviceId, EventId, ServerName, UserId}, identifiers::{DeviceId, EventId, RoomId, ServerName, UserId},
reqwest::Url, reqwest::Url,
Client, ClientConfig, LoopCtrl, SyncSettings, Client, ClientConfig, LoopCtrl, SyncSettings,
}; };
@ -171,6 +172,7 @@ pub fn parse_mxc(url: &str) -> Result<(Box<ServerName>, String), Error> {
} }
} }
/// Makes an iced subscription for listening for matrix events.
pub struct MatrixSync { pub struct MatrixSync {
client: matrix_sdk::Client, client: matrix_sdk::Client,
join: Option<tokio::task::JoinHandle<()>>, join: Option<tokio::task::JoinHandle<()>>,
@ -183,52 +185,21 @@ impl MatrixSync {
} }
} }
/// A matrix event that should be passed to the iced subscription
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Event { pub enum Event {
Room(AnyRoomEvent), /// An event for an invited room
Invited(AnyRoomEvent, Arc<matrix_sdk::room::Invited>),
/// An event for a joined room
Joined(AnyRoomEvent, Arc<matrix_sdk::room::Joined>),
/// An event for a left room
Left(AnyRoomEvent, Arc<matrix_sdk::room::Left>),
/// A to-device event
ToDevice(AnyToDeviceEvent), ToDevice(AnyToDeviceEvent),
/// Synchronization token
Token(String), Token(String),
} }
/*pub enum Sync {
MessageInvited()
MessageJoined(RoomId, AnyRoomEvent),
MessageLeft(RoomId, AnyRoomEvent),
}
struct Emitter {
client: Client,
sender: tokio::sync::mpsc::Sender<Sync>,
}
#[async_trait]
impl matrix_sdk::EventEmitter for Emitter {
async fn on_room_message(
&self,
room: RoomState,
message: &SyncMessageEvent<MessageEventContent>,
) {
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<MemberEventContent>) {
}
}*/
impl<H, I> iced_futures::subscription::Recipe<H, I> for MatrixSync impl<H, I> iced_futures::subscription::Recipe<H, I> for MatrixSync
where where
H: std::hash::Hasher, H: std::hash::Hasher,
@ -255,33 +226,16 @@ where
.token(client.sync_token().await.unwrap()) .token(client.sync_token().await.unwrap())
.timeout(Duration::from_secs(30)), .timeout(Duration::from_secs(30)),
|response| async { |response| async {
//sender.send(Event::Token(response.next_batch)).ok();
for (id, room) in response.rooms.join { for (id, room) in response.rooms.join {
let joined = Arc::new(client.get_joined_room(&id).unwrap());
for event in room.state.events { for event in room.state.events {
let id = id.clone(); let id = id.clone();
sender let event = AnyRoomEvent::State(event.into_full_event(id));
.send(Event::Room(AnyRoomEvent::State( sender.send(Event::Joined(event, Arc::clone(&joined))).ok();
event.into_full_event(id),
)))
.ok();
} }
for event in room.timeline.events { for event in room.timeline.events {
let id = id.clone(); let event = event.into_full_event(id.clone());
let event = match event { sender.send(Event::Joined(event, Arc::clone(&joined))).ok();
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();
} }
} }
for event in response.to_device.events { for event in response.to_device.events {
@ -345,10 +299,33 @@ impl AnyMessageEventExt for AnyMessageEvent {
fn image_url(&self) -> Option<String> { fn image_url(&self) -> Option<String> {
match self { match self {
AnyMessageEvent::RoomMessage(MessageEvent { AnyMessageEvent::RoomMessage(MessageEvent {
content: MessageEventContent::Image(ref image), content:
MessageEventContent {
msgtype: MessageType::Image(ref image),
..
},
.. ..
}) => image.url.clone(), }) => image.url.clone(),
_ => None, _ => 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))
}
}
}
}

541
src/ui.rs
View File

@ -17,7 +17,7 @@ use matrix_sdk::{
key::verification::cancel::CancelCode as VerificationCancelCode, key::verification::cancel::CancelCode as VerificationCancelCode,
room::{ room::{
member::MembershipState, member::MembershipState,
message::{MessageEventContent, Relation, TextMessageEventContent}, message::{MessageEventContent, MessageType, Relation},
}, },
AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent, AnyStateEvent, AnyToDeviceEvent, AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent, AnyStateEvent, AnyToDeviceEvent,
}, },
@ -28,10 +28,13 @@ use crate::matrix::{self, AnyMessageEventExt, AnyRoomEventExt};
pub mod prompt; pub mod prompt;
pub mod settings; pub mod settings;
pub mod theme;
use prompt::{PromptAction, PromptView}; use prompt::{PromptAction, PromptView};
use settings::SettingsView; use settings::SettingsView;
const THUMBNAIL_SIZE: u32 = 48;
/// What order to sort rooms in in the room list. /// What order to sort rooms in in the room list.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RoomSorting { pub enum RoomSorting {
@ -59,10 +62,10 @@ pub struct RoomEntry {
} }
impl 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 { Self {
direct: room.direct_target(), direct: room.direct_target(),
name: room.display_name().await, name: room.display_name().await.unwrap(),
topic: room.topic().unwrap_or_default(), topic: room.topic().unwrap_or_default(),
alias: room.canonical_alias(), alias: room.canonical_alias(),
avatar: room.avatar_url(), avatar: room.avatar_url(),
@ -116,10 +119,10 @@ impl MessageBuffer {
if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage( if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(
matrix_sdk::events::MessageEvent { matrix_sdk::events::MessageEvent {
content: content:
MessageEventContent::Text(TextMessageEventContent { MessageEventContent {
relates_to: Some(Relation::Replacement(ref replacement)), relates_to: Some(Relation::Replacement(ref replacement)),
.. ..
}), },
.. ..
}, },
)) = event )) = event
@ -142,10 +145,10 @@ impl MessageBuffer {
if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage( if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(
matrix_sdk::events::MessageEvent { matrix_sdk::events::MessageEvent {
content: content:
MessageEventContent::Text(TextMessageEventContent { MessageEventContent {
relates_to: Some(Relation::Replacement(replacement)), relates_to: Some(Relation::Replacement(replacement)),
.. ..
}), },
.. ..
}, },
)) = event )) = event
@ -207,6 +210,8 @@ pub struct MainView {
rooms: BTreeMap<RoomId, RoomEntry>, rooms: BTreeMap<RoomId, RoomEntry>,
/// A map of mxc urls to image data /// A map of mxc urls to image data
images: BTreeMap<String, iced::image::Handle>, images: BTreeMap<String, iced::image::Handle>,
/// A map of mxc urls to image thumbnails
thumbnails: BTreeMap<String, iced::image::Handle>,
/// Room list entries for direct conversations /// Room list entries for direct conversations
dm_buttons: Vec<iced::button::State>, dm_buttons: Vec<iced::button::State>,
@ -245,6 +250,7 @@ impl MainView {
rooms: Default::default(), rooms: Default::default(),
selected: None, selected: None,
images: Default::default(), images: Default::default(),
thumbnails: Default::default(),
room_scroll: Default::default(), room_scroll: Default::default(),
message_scroll: Default::default(), message_scroll: Default::default(),
backfill_button: Default::default(), backfill_button: Default::default(),
@ -298,7 +304,7 @@ impl MainView {
Vec<(&RoomId, &RoomEntry)>, Vec<(&RoomId, &RoomEntry)>,
) = rooms ) = rooms
.iter() .iter()
// Hide if we're in the room the tombstone points to // Hide if have joined the room the tombstone points to
.filter(|(id, _)| { .filter(|(id, _)| {
!client !client
.get_joined_room(id) .get_joined_room(id)
@ -324,6 +330,7 @@ impl MainView {
self.group_buttons self.group_buttons
.resize_with(group_rooms.len(), Default::default); .resize_with(group_rooms.len(), Default::default);
// Create buttons // Create buttons
let thumbnails = &self.thumbnails;
let images = &self.images; let images = &self.images;
let dm_buttons: Vec<Button<_>> = self let dm_buttons: Vec<Button<_>> = self
.dm_buttons .dm_buttons
@ -333,13 +340,13 @@ impl MainView {
// TODO: highlight selected // TODO: highlight selected
let (id, room) = unsafe { dm_rooms.get_unchecked(idx) }; let (id, room) = unsafe { dm_rooms.get_unchecked(idx) };
let name = if room.name.is_empty() { let name = if room.name.is_empty() {
"Missing name" "Empty room"
} else { } else {
&room.name &room.name
}; };
let mut row = Row::new().align_items(Align::Center); let mut row = Row::new().align_items(Align::Center);
if let Some(ref url) = room.avatar { if let Some(ref url) = room.avatar {
if let Some(handle) = images.get(url) { if let Some(handle) = thumbnails.get(url) {
row = row.push( row = row.push(
Image::new(handle.clone()) Image::new(handle.clone())
.width(20.into()) .width(20.into())
@ -467,32 +474,30 @@ impl MainView {
sender = match block_on(async { sender = match block_on(async {
joined.get_member(&message.sender).await joined.get_member(&message.sender).await
}) { }) {
Some(member) => member.name().to_owned(), Ok(Some(member)) => member.name().to_owned(),
None => message.sender.to_string(), _ => message.sender.to_string(),
}; };
scroll = scroll scroll = scroll
.push(iced::Space::with_height(4.into())) .push(iced::Space::with_height(4.into()))
.push(Text::new(&sender).color([0.0, 0.0, 1.0])); .push(Text::new(&sender).color([0.0, 0.0, 1.0]));
} }
let content: Element<_> = match &message.content { let content: Element<_> = match &message.content.msgtype {
MessageEventContent::Audio(audio) => { MessageType::Audio(audio) => {
Text::new(format!("Audio message: {}", audio.body)) Text::new(format!("Audio message: {}", audio.body))
.color([0.2, 0.2, 0.2]) .color([0.2, 0.2, 0.2])
.width(Length::Fill) .width(Length::Fill)
.into() .into()
} }
MessageEventContent::Emote(emote) => { MessageType::Emote(emote) => {
Text::new(format!("* {} {}", sender, emote.body)) Text::new(format!("* {} {}", sender, emote.body))
.width(Length::Fill) .width(Length::Fill)
.into() .into()
} }
MessageEventContent::File(file) => { MessageType::File(file) => Text::new(format!("File '{}'", file.body))
Text::new(format!("File '{}'", file.body))
.color([0.2, 0.2, 0.2]) .color([0.2, 0.2, 0.2])
.width(Length::Fill) .width(Length::Fill)
.into() .into(),
} MessageType::Image(image) => {
MessageEventContent::Image(image) => {
if let Some(ref url) = image.url { if let Some(ref url) = image.url {
match self.images.get(url) { match self.images.get(url) {
Some(handle) => Container::new( Some(handle) => Container::new(
@ -512,16 +517,16 @@ impl MainView {
.into() .into()
} }
} }
MessageEventContent::Notice(notice) => { MessageType::Notice(notice) => {
Text::new(&notice.body).width(Length::Fill).into() Text::new(&notice.body).width(Length::Fill).into()
} }
MessageEventContent::ServerNotice(notice) => { MessageType::ServerNotice(notice) => {
Text::new(&notice.body).width(Length::Fill).into() Text::new(&notice.body).width(Length::Fill).into()
} }
MessageEventContent::Text(text) => { MessageType::Text(text) => {
Text::new(&text.body).width(Length::Fill).into() Text::new(&text.body).width(Length::Fill).into()
} }
MessageEventContent::Video(video) => { MessageType::Video(video) => {
Text::new(format!("Video: {}", video.body)) Text::new(format!("Video: {}", video.body))
.color([0.2, 0.2, 0.2]) .color([0.2, 0.2, 0.2])
.into() .into()
@ -658,206 +663,10 @@ impl MainView {
root_row.into() root_row.into()
} }
}
#[allow(clippy::large_enum_variant)] fn update(&mut self, message: Message) -> Command<Message> {
#[derive(Debug, Clone)] let view = self;
pub enum Retrix { match message {
Prompt(PromptView),
AwaitLogin,
LoggedIn(MainView),
}
#[derive(Debug, Clone)]
pub enum Message {
// Login form messages
SetUser(String),
SetPassword(String),
SetServer(String),
SetDeviceName(String),
SetAction(PromptAction),
Login,
Signup,
// Auth result messages
LoggedIn(matrix_sdk::Client, matrix::Session),
LoginFailed(String),
// Main state messages
/// Reset state for room
ResetRoom(RoomId, RoomEntry),
RoomName(RoomId, String),
/// Get backfill for given room
BackFill(RoomId),
/// Received backfill
BackFilled(RoomId, MessageResponse),
/// Fetch an image pointed to by an mxc url
FetchImage(String),
/// Fetched an image
FetchedImage(String, iced::image::Handle),
/// View messages from this room
SelectRoom(RoomId),
/// Set error message
ErrorMessage(String),
/// Close error message
ClearError,
/// Set how the room list is sorted
SetSort(RoomSorting),
/// Set verification flow
SetVerification(Option<matrix_sdk::Sas>),
/// Accept verification flow
VerificationAccept,
/// Accept sent
VerificationAccepted,
/// Confirm keys match
VerificationConfirm,
/// Confirmation sent
VerificationConfirmed,
/// Cancel verification flow
VerificationCancel,
/// Verification flow cancelled
VerificationCancelled(VerificationCancelCode),
/// Close verification bar
VerificationClose,
/// Matrix event received
Sync(matrix::Event),
/// Update the sync token to use
SyncToken(String),
/// Set contents of message compose box
SetMessage(String),
/// Send the contents of the compose box to the selected room
SendMessage,
// Settings messages
/// Open settings menu
OpenSettings,
/// Close settings menu
CloseSettings,
/// Set display name input field
SetDisplayNameInput(String),
/// Save new display name
SaveDisplayName,
/// New display name saved successfully
DisplayNameSaved,
/// Set key import path
SetKeyPath(String),
/// Set password key backup is encrypted with
SetKeyPassword(String),
/// Import encryption keys
ImportKeys,
}
impl Application for Retrix {
type Message = Message;
type Executor = iced::executor::Default;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Self::Message>) {
// Skip login prompt if we have a session saved
match matrix::get_session().ok().flatten() {
Some(session) => {
let command = Command::perform(
async move { matrix::restore_login(session).await },
|result| match result {
Ok((s, c)) => Message::LoggedIn(s, c),
Err(e) => Message::LoginFailed(e.to_string()),
},
);
(Retrix::AwaitLogin, command)
}
None => (Retrix::Prompt(PromptView::new()), Command::none()),
}
}
fn title(&self) -> String {
String::from("Retrix matrix client")
}
fn subscription(&self) -> Subscription<Self::Message> {
match self {
Retrix::LoggedIn(view) => {
matrix::MatrixSync::subscription(view.client.clone()).map(Message::Sync)
}
_ => Subscription::none(),
}
}
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match self {
Retrix::Prompt(prompt) => match message {
Message::SetUser(u) => prompt.user = u,
Message::SetPassword(p) => prompt.password = p,
Message::SetServer(s) => prompt.server = s,
Message::SetDeviceName(n) => prompt.device_name = n,
Message::SetAction(a) => prompt.action = a,
Message::Login => {
let user = prompt.user.clone();
let password = prompt.password.clone();
let server = prompt.server.clone();
let device = prompt.device_name.clone();
let device = match device.is_empty() {
false => Some(device),
true => None,
};
*self = Retrix::AwaitLogin;
return Command::perform(
async move { matrix::login(&user, &password, &server, device.as_deref()).await },
|result| match result {
Ok((c, r)) => Message::LoggedIn(c, r),
Err(e) => Message::LoginFailed(e.to_string()),
},
);
}
Message::Signup => {
let user = prompt.user.clone();
let password = prompt.password.clone();
let server = prompt.server.clone();
let device = prompt.device_name.clone();
let device = match device.is_empty() {
false => Some(device),
true => None,
};
*self = Retrix::AwaitLogin;
return Command::perform(
async move {
matrix::signup(&user, &password, &server, device.as_deref()).await
},
|result| match result {
Ok((client, response)) => Message::LoggedIn(client, response),
Err(e) => Message::LoginFailed(e.to_string()),
},
);
}
_ => (),
},
Retrix::AwaitLogin => match message {
Message::LoginFailed(e) => {
let view = PromptView {
error: Some(e),
..PromptView::default()
};
*self = Retrix::Prompt(view);
}
Message::LoggedIn(client, session) => {
*self = Retrix::LoggedIn(MainView::new(client.clone(), session));
let mut commands: Vec<Command<Message>> = Vec::new();
for room in client.joined_rooms().into_iter() {
let room = std::sync::Arc::new(room);
let r = room.clone();
let command: Command<_> = async move {
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);
}
_ => (),
},
Retrix::LoggedIn(view) => match message {
Message::ErrorMessage(e) => view.error = Some((e, Default::default())), Message::ErrorMessage(e) => view.error = Some((e, Default::default())),
Message::ClearError => view.error = None, Message::ClearError => view.error = None,
Message::SetSort(s) => view.sorting = s, Message::SetSort(s) => view.sorting = s,
@ -872,27 +681,19 @@ impl Application for Retrix {
} }
} }
Message::Sync(event) => match event { Message::Sync(event) => match event {
matrix::Event::Room(event) => match event { matrix::Event::Joined(event, joined) => match event {
AnyRoomEvent::Message(event) => { AnyRoomEvent::Message(event) => {
let room = view.rooms.entry(event.room_id().clone()).or_default(); let room = view.rooms.entry(event.room_id().clone()).or_default();
room.messages.push(AnyRoomEvent::Message(event.clone())); room.messages.push(AnyRoomEvent::Message(event.clone()));
let mut commands = Vec::new(); let mut commands = Vec::new();
// Add fetch image command if the message has an image
let img_cmd = match event.image_url() { let img_cmd = match event.image_url() {
Some(url) => async { Message::FetchImage(url) }.into(), Some(url) => async { Message::FetchImage(url) }.into(),
None => Command::none(), None => Command::none(),
}; };
commands.push(img_cmd); commands.push(img_cmd);
// Set read marker if message is in selected room // Set read marker if message is in selected room
if view.selected.as_ref() == Some(event.room_id()) { /*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>, f32)>(
view.message_scroll,
);
tmp.1 = 999999.0;
std::mem::transmute::<_, iced::scrollable::State>(tmp)
};*/
let client = view.client.clone(); let client = view.client.clone();
let marker_cmd = async move { let marker_cmd = async move {
let result = client let result = client
@ -911,7 +712,8 @@ impl Application for Retrix {
} }
.into(); .into();
commands.push(marker_cmd); commands.push(marker_cmd);
} }*/
return Command::batch(commands); return Command::batch(commands);
} }
AnyRoomEvent::State(event) => match event { AnyRoomEvent::State(event) => match event {
@ -928,7 +730,10 @@ impl Application for Retrix {
let client = view.client.clone(); let client = view.client.clone();
return async move { return async move {
let joined = client.get_joined_room(&id).unwrap(); let joined = client.get_joined_room(&id).unwrap();
Message::RoomName(id, joined.display_name().await) match joined.display_name().await {
Ok(name) => Message::RoomName(id, name),
Err(e) => Message::ErrorMessage(e.to_string()),
}
} }
.into(); .into();
} }
@ -942,7 +747,8 @@ impl Application for Retrix {
room.messages.push(AnyRoomEvent::State(event)); room.messages.push(AnyRoomEvent::State(event));
if let Some(url) = room.avatar.clone() { if let Some(url) = room.avatar.clone() {
room.avatar = Some(url.clone()); room.avatar = Some(url.clone());
return async { Message::FetchImage(url) }.into(); //return async { Message::FetchThumb(url) }.into();
return Command::none();
} }
} }
AnyStateEvent::RoomCreate(ref create) => { AnyStateEvent::RoomCreate(ref create) => {
@ -1004,9 +810,7 @@ impl Application for Retrix {
AnyToDeviceEvent::KeyVerificationStart(start) => { AnyToDeviceEvent::KeyVerificationStart(start) => {
let client = view.client.clone(); let client = view.client.clone();
return Command::perform( return Command::perform(
async move { async move { client.get_verification(&start.content.transaction_id).await },
client.get_verification(&start.content.transaction_id).await
},
Message::SetVerification, Message::SetVerification,
); );
} }
@ -1019,23 +823,23 @@ impl Application for Retrix {
matrix::Event::Token(token) => { matrix::Event::Token(token) => {
view.sync_token = token; view.sync_token = token;
} }
_ => (),
}, },
Message::BackFill(id) => { Message::BackFill(id) => {
let room = view.rooms.entry(id.clone()).or_default(); let entry = view.rooms.entry(id.clone()).or_default();
room.messages.loading = true; entry.messages.loading = true;
let client = view.client.clone(); let client = view.client.clone();
let token = match room.messages.end.clone() { let room = client.get_joined_room(&id).unwrap();
let token = match entry.messages.end.clone() {
Some(end) => end, Some(end) => end,
None => client None => room
.get_joined_room(&id)
.unwrap()
.last_prev_batch() .last_prev_batch()
.unwrap_or_else(|| view.sync_token.clone()), .unwrap_or_else(|| view.sync_token.clone()),
}; };
return async move { return async move {
let mut request = MessageRequest::backward(&id, &token); let mut request = MessageRequest::backward(&id, &token);
request.limit = matrix_sdk::uint!(30); request.limit = matrix_sdk::uint!(30);
match client.room_messages(request).await { match room.messages(request).await {
Ok(response) => Message::BackFilled(id, response), Ok(response) => Message::BackFilled(id, response),
Err(e) => Message::ErrorMessage(e.to_string()), Err(e) => Message::ErrorMessage(e.to_string()),
} }
@ -1071,17 +875,16 @@ impl Application for Retrix {
room.messages.append(events); room.messages.append(events);
return Command::batch(commands); return Command::batch(commands);
} }
Message::FetchImage(url) => { Message::FetchImage(url) => {
let (server, path) = match matrix::parse_mxc(&url) { let (server, path) = match matrix::parse_mxc(&url) {
Ok((server, path)) => (server, path), Ok((server, path)) => (server, path),
Err(e) => { Err(e) => return async move { Message::ErrorMessage(e.to_string()) }.into(),
return async move { Message::ErrorMessage(e.to_string()) }.into()
}
}; };
let client = view.client.clone(); let client = view.client.clone();
return async move { return async move {
let request = ImageRequest::new(&path, &*server); let request = ImageRequest::new(&path, &*server);
let response = client.send(request).await; let response = client.send(request, None).await;
match response { match response {
Ok(response) => Message::FetchedImage( Ok(response) => Message::FetchedImage(
url, url,
@ -1095,6 +898,9 @@ impl Application for Retrix {
Message::FetchedImage(url, handle) => { Message::FetchedImage(url, handle) => {
view.images.insert(url, handle); view.images.insert(url, handle);
} }
Message::FetchedThumbnail(url, handle) => {
view.thumbnails.insert(url, handle);
}
Message::RoomName(id, name) => { Message::RoomName(id, name) => {
if let Some(room) = view.rooms.get_mut(&id) { if let Some(room) = view.rooms.get_mut(&id) {
room.name = name; room.name = name;
@ -1119,12 +925,13 @@ impl Application for Retrix {
Some(sas) => sas.clone(), Some(sas) => sas.clone(),
None => return Command::none(), None => return Command::none(),
}; };
return Command::perform(async move { sas.confirm().await }, |result| { return Command::perform(
match result { async move { sas.confirm().await },
|result| match result {
Ok(()) => Message::VerificationConfirmed, Ok(()) => Message::VerificationConfirmed,
Err(e) => Message::ErrorMessage(e.to_string()), Err(e) => Message::ErrorMessage(e.to_string()),
} },
}); );
} }
Message::VerificationCancel => { Message::VerificationCancel => {
let sas = match &view.sas { let sas = match &view.sas {
@ -1236,7 +1043,231 @@ impl Application for Retrix {
} }
Message::CloseSettings => view.settings_view = None, Message::CloseSettings => view.settings_view = None,
_ => (), _ => (),
};
Command::none()
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
pub enum Retrix {
Prompt(PromptView),
AwaitLogin,
LoggedIn(MainView),
}
#[derive(Debug, Clone)]
pub enum Message {
/// Do nothing
Noop,
// Login form messages
SetUser(String),
SetPassword(String),
SetServer(String),
SetDeviceName(String),
SetAction(PromptAction),
Login,
Signup,
// Auth result messages
LoggedIn(matrix_sdk::Client, matrix::Session),
LoginFailed(String),
// Main state messages
/// Reset state for room
ResetRoom(RoomId, RoomEntry),
RoomName(RoomId, String),
/// Get backfill for given room
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
FetchedImage(String, iced::image::Handle),
/// View messages from this room
SelectRoom(RoomId),
/// Set error message
ErrorMessage(String),
/// Close error message
ClearError,
/// Set how the room list is sorted
SetSort(RoomSorting),
/// Set verification flow
SetVerification(Option<matrix_sdk::Sas>),
/// Accept verification flow
VerificationAccept,
/// Accept sent
VerificationAccepted,
/// Confirm keys match
VerificationConfirm,
/// Confirmation sent
VerificationConfirmed,
/// Cancel verification flow
VerificationCancel,
/// Verification flow cancelled
VerificationCancelled(VerificationCancelCode),
/// Close verification bar
VerificationClose,
/// Matrix event received
Sync(matrix::Event),
/// Update the sync token to use
SyncToken(String),
/// Set contents of message compose box
SetMessage(String),
/// Send the contents of the compose box to the selected room
SendMessage,
// Settings messages
/// Open settings menu
OpenSettings,
/// Close settings menu
CloseSettings,
/// Set display name input field
SetDisplayNameInput(String),
/// Save new display name
SaveDisplayName,
/// New display name saved successfully
DisplayNameSaved,
/// Set key import path
SetKeyPath(String),
/// Set password key backup is encrypted with
SetKeyPassword(String),
/// Import encryption keys
ImportKeys,
}
impl Application for Retrix {
type Message = Message;
type Executor = iced::executor::Default;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Self::Message>) {
// Skip login prompt if we have a session saved
match matrix::get_session().ok().flatten() {
Some(session) => {
let command = Command::perform(
async move { matrix::restore_login(session).await },
|result| match result {
Ok((s, c)) => Message::LoggedIn(s, c),
Err(e) => Message::LoginFailed(e.to_string()),
}, },
);
(Retrix::AwaitLogin, command)
}
None => (Retrix::Prompt(PromptView::new()), Command::none()),
}
}
fn title(&self) -> String {
String::from("Retrix matrix client")
}
fn subscription(&self) -> Subscription<Self::Message> {
match self {
Retrix::LoggedIn(view) => {
matrix::MatrixSync::subscription(view.client.clone()).map(Message::Sync)
}
_ => Subscription::none(),
}
}
fn update(
&mut self,
message: Self::Message,
_clipboard: &mut iced::Clipboard,
) -> Command<Self::Message> {
match self {
Retrix::Prompt(prompt) => match message {
Message::SetUser(u) => prompt.user = u,
Message::SetPassword(p) => prompt.password = p,
Message::SetServer(s) => prompt.server = s,
Message::SetDeviceName(n) => prompt.device_name = n,
Message::SetAction(a) => prompt.action = a,
Message::Login => {
let user = prompt.user.clone();
let password = prompt.password.clone();
let server = prompt.server.clone();
let device = prompt.device_name.clone();
let device = match device.is_empty() {
false => Some(device),
true => None,
};
*self = Retrix::AwaitLogin;
return Command::perform(
async move { matrix::login(&user, &password, &server, device.as_deref()).await },
|result| match result {
Ok((c, r)) => Message::LoggedIn(c, r),
Err(e) => Message::LoginFailed(e.to_string()),
},
);
}
Message::Signup => {
let user = prompt.user.clone();
let password = prompt.password.clone();
let server = prompt.server.clone();
let device = prompt.device_name.clone();
let device = match device.is_empty() {
false => Some(device),
true => None,
};
*self = Retrix::AwaitLogin;
return Command::perform(
async move {
matrix::signup(&user, &password, &server, device.as_deref()).await
},
|result| match result {
Ok((client, response)) => Message::LoggedIn(client, response),
Err(e) => Message::LoginFailed(e.to_string()),
},
);
}
_ => (),
},
Retrix::AwaitLogin => match message {
Message::LoginFailed(e) => {
let view = PromptView {
error: Some(e),
..PromptView::default()
};
*self = Retrix::Prompt(view);
}
Message::LoggedIn(client, session) => {
*self = Retrix::LoggedIn(MainView::new(client.clone(), session));
let mut commands: Vec<Command<Message>> = Vec::new();
for room in client.joined_rooms().into_iter() {
let room = std::sync::Arc::new(room);
let r = room.clone();
let command: Command<_> = async move {
let entry = RoomEntry::from_sdk(&r).await;
Message::ResetRoom(r.room_id().to_owned(), entry)
}
.into();
commands.push(command);
// 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),
),
Ok(None) => Message::Noop,
Err(e) => Message::ErrorMessage(e.to_string()),
}
}
.into(),
)
}
return Command::batch(commands);
}
_ => (),
},
Retrix::LoggedIn(view) => return view.update(message),
}; };
Command::none() Command::none()
} }

6
src/ui/theme.rs Normal file
View File

@ -0,0 +1,6 @@
//! Theming for widgets.
/// Which colorscheme to use
pub enum Theme {
Default,
}