update dependencies, code restructuring, use thumbnails for rooms
This commit is contained in:
parent
a8b76eaded
commit
fe3d384435
13
Cargo.toml
13
Cargo.toml
|
@ -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"
|
||||||
|
|
16
README.md
16
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.
|
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
|
||||||
|
|
107
src/matrix.rs
107
src/matrix.rs
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
851
src/ui.rs
851
src/ui.rs
|
@ -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(¬ice.body).width(Length::Fill).into()
|
Text::new(¬ice.body).width(Length::Fill).into()
|
||||||
}
|
}
|
||||||
MessageEventContent::ServerNotice(notice) => {
|
MessageType::ServerNotice(notice) => {
|
||||||
Text::new(¬ice.body).width(Length::Fill).into()
|
Text::new(¬ice.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,6 +663,389 @@ impl MainView {
|
||||||
|
|
||||||
root_row.into()
|
root_row.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Command<Message> {
|
||||||
|
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<AnyRoomEvent> = 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<Command<_>> = 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)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
@ -670,6 +1058,8 @@ pub enum Retrix {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
|
/// Do nothing
|
||||||
|
Noop,
|
||||||
// Login form messages
|
// Login form messages
|
||||||
SetUser(String),
|
SetUser(String),
|
||||||
SetPassword(String),
|
SetPassword(String),
|
||||||
|
@ -690,6 +1080,8 @@ pub enum Message {
|
||||||
BackFill(RoomId),
|
BackFill(RoomId),
|
||||||
/// Received backfill
|
/// Received backfill
|
||||||
BackFilled(RoomId, MessageResponse),
|
BackFilled(RoomId, MessageResponse),
|
||||||
|
/// Fetched a thumbnail
|
||||||
|
FetchedThumbnail(String, iced::image::Handle),
|
||||||
/// Fetch an image pointed to by an mxc url
|
/// Fetch an image pointed to by an mxc url
|
||||||
FetchImage(String),
|
FetchImage(String),
|
||||||
/// Fetched an image
|
/// Fetched an image
|
||||||
|
@ -781,7 +1173,11 @@ impl Application for Retrix {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
message: Self::Message,
|
||||||
|
_clipboard: &mut iced::Clipboard,
|
||||||
|
) -> Command<Self::Message> {
|
||||||
match self {
|
match self {
|
||||||
Retrix::Prompt(prompt) => match message {
|
Retrix::Prompt(prompt) => match message {
|
||||||
Message::SetUser(u) => prompt.user = u,
|
Message::SetUser(u) => prompt.user = u,
|
||||||
|
@ -848,395 +1244,30 @@ impl Application for Retrix {
|
||||||
Message::ResetRoom(r.room_id().to_owned(), entry)
|
Message::ResetRoom(r.room_id().to_owned(), entry)
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
if let Some(url) = room.avatar_url() {
|
|
||||||
commands.push(async { Message::FetchImage(url) }.into())
|
|
||||||
}
|
|
||||||
commands.push(command);
|
commands.push(command);
|
||||||
}
|
// Fetch room avatar thumbnail if available
|
||||||
return Command::batch(commands);
|
commands.push(
|
||||||
}
|
async move {
|
||||||
_ => (),
|
match room
|
||||||
},
|
.avatar(Some(THUMBNAIL_SIZE), Some(THUMBNAIL_SIZE))
|
||||||
Retrix::LoggedIn(view) => match message {
|
.await
|
||||||
Message::ErrorMessage(e) => view.error = Some((e, Default::default())),
|
{
|
||||||
Message::ClearError => view.error = None,
|
Ok(Some(avatar)) => Message::FetchedThumbnail(
|
||||||
Message::SetSort(s) => view.sorting = s,
|
room.avatar_url().unwrap(),
|
||||||
Message::ResetRoom(id, room) => {
|
iced::image::Handle::from_memory(avatar),
|
||||||
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>, 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<AnyRoomEvent> = 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<Command<_>> = 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),
|
|
||||||
),
|
),
|
||||||
None,
|
Ok(None) => Message::Noop,
|
||||||
)
|
Err(e) => Message::ErrorMessage(e.to_string()),
|
||||||
.await
|
}
|
||||||
},
|
}
|
||||||
|result| match result {
|
.into(),
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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()
|
Command::none()
|
||||||
}
|
}
|
||||||
|
|
6
src/ui/theme.rs
Normal file
6
src/ui/theme.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
//! Theming for widgets.
|
||||||
|
|
||||||
|
/// Which colorscheme to use
|
||||||
|
pub enum Theme {
|
||||||
|
Default,
|
||||||
|
}
|
Loading…
Reference in a new issue