Intial image support, proper time formatting

Also feature gate unix exclusive code
This commit is contained in:
Amanda Graven 2021-01-09 19:11:10 +01:00
parent 2a9dbb90d3
commit f25d3b6821
Signed by: amanda
GPG key ID: 45C461CDC9286390
4 changed files with 315 additions and 132 deletions

View file

@ -8,22 +8,22 @@ edition = "2018"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
async-trait = "0.1" async-stream = "0.3"
crossbeam-channel = "0.4"
dirs-next = "2.0" dirs-next = "2.0"
futures = "0.3" futures = "0.3"
iced = { version = "0.2", features = ["debug", "tokio_old", "image"] } iced = { git = "https://github.com/hecrj/iced", rev = "31522e3", features = ["debug", "image", "tokio"] }
iced_futures = "0.2" iced_futures = { git = "https://github.com/hecrj/iced", rev = "31522e3" }
hostname = "0.3" #iced_glow = { git = "https://github.com/hecrj/iced", rev = "31522e3", features = ["image"] }
#matrix-sdk-common-macros = { git = "https://github.com/matrix-org/matrix-rust-sdk", rev = "e65915e" } #matrix-sdk-common-macros = { git = "https://github.com/matrix-org/matrix-rust-sdk", rev = "e65915e" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tokio = { version = "0.2", features = ["sync"] } time = "0.2"
tokio = { version = "1.0", 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 = "74998c8" rev = "6435269"
default_features = false default_features = false
features = ["encryption", "sqlite_cryptostore", "messages", "rustls-tls", "unstable-synapse-quirks"] features = ["encryption", "sqlite_cryptostore", "messages", "rustls-tls", "unstable-synapse-quirks"]

View file

@ -1,8 +1,7 @@
extern crate crossbeam_channel as channel;
extern crate dirs_next as dirs; extern crate dirs_next as dirs;
use std::fs::Permissions; #[cfg(unix)]
use std::os::unix::fs::PermissionsExt; use std::{fs::Permissions, os::unix::fs::PermissionsExt};
use iced::Application; use iced::Application;
@ -16,6 +15,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Make sure config dir exists and is not accessible by other users. // Make sure config dir exists and is not accessible by other users.
if !config_dir.is_dir() { if !config_dir.is_dir() {
std::fs::create_dir(&config_dir)?; std::fs::create_dir(&config_dir)?;
#[cfg(unix)]
std::fs::set_permissions(&config_dir, Permissions::from_mode(0o700))?; std::fs::set_permissions(&config_dir, Permissions::from_mode(0o700))?;
} }

View file

@ -1,9 +1,16 @@
use std::time::{Duration, SystemTime}; use std::{
convert::TryFrom,
time::{Duration, SystemTime},
};
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::{AnyRoomEvent, AnySyncRoomEvent, AnyToDeviceEvent}, events::{
identifiers::{DeviceId, EventId, UserId}, room::message::{MessageEvent, MessageEventContent},
AnyMessageEvent, AnyRoomEvent, AnySyncRoomEvent, AnyToDeviceEvent,
},
identifiers::{DeviceId, EventId, ServerName, UserId},
reqwest::Url, reqwest::Url,
Client, ClientConfig, LoopCtrl, SyncSettings, Client, ClientConfig, LoopCtrl, SyncSettings,
}; };
@ -67,6 +74,7 @@ pub async fn signup(
}); });
let response = client.register(request).await?; let response = client.register(request).await?;
client.sync_once(SyncSettings::new()).await?;
let session = Session { let session = Session {
access_token: response.access_token.unwrap(), access_token: response.access_token.unwrap(),
@ -103,7 +111,7 @@ pub async fn login(
homeserver: server.to_owned(), homeserver: server.to_owned(),
}; };
write_session(&session)?; write_session(&session)?;
//client.sync_once(SyncSettings::new()).await?; client.sync_once(SyncSettings::new()).await?;
Ok((client, session)) Ok((client, session))
} }
@ -150,6 +158,19 @@ fn write_session(session: &Session) -> Result<(), Error> {
Ok(()) Ok(())
} }
/// Break down an mxc url to its authority and path
pub fn parse_mxc(url: &str) -> Result<(Box<ServerName>, String), Error> {
let url = Url::parse(&url)?;
anyhow::ensure!(url.scheme() == "mxc", "Not an mxc url");
let host = url.host_str().ok_or(anyhow::anyhow!("url"))?;
let server_name: Box<ServerName> = <&ServerName>::try_from(host)?.into();
let path = url.path_segments().and_then(|mut p| p.next());
match path {
Some(path) => Ok((server_name, path.to_owned())),
_ => Err(anyhow::anyhow!("Invalid mxc url")),
}
}
pub struct MatrixSync { pub struct MatrixSync {
client: matrix_sdk::Client, client: matrix_sdk::Client,
join: Option<tokio::task::JoinHandle<()>>, join: Option<tokio::task::JoinHandle<()>>,
@ -162,21 +183,6 @@ impl MatrixSync {
} }
} }
/*#[async_trait]
impl EventEmitter for Callback {
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
let room_id = if let matrix_sdk::RoomState::Joined(arc) = room {
let room = arc.read().await;
room.room_id.clone()
} else {
return;
};
self.sender
.send(event.clone().into_full_event(room_id))
.ok();
}
}*/
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Event { pub enum Event {
Room(AnyRoomEvent), Room(AnyRoomEvent),
@ -201,13 +207,13 @@ where
mut self: Box<Self>, mut self: Box<Self>,
_input: iced_futures::BoxStream<I>, _input: iced_futures::BoxStream<I>,
) -> iced_futures::BoxStream<Self::Output> { ) -> iced_futures::BoxStream<Self::Output> {
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
let client = self.client.clone(); let client = self.client.clone();
let join = tokio::task::spawn(async move { let join = tokio::task::spawn(async move {
client client
.sync_with_callback( .sync_with_callback(
SyncSettings::new() SyncSettings::new()
//.token(client.sync_token().await.unwrap()) .token(client.sync_token().await.unwrap())
.timeout(Duration::from_secs(90)) .timeout(Duration::from_secs(90))
.full_state(true), .full_state(true),
|response| async { |response| async {
@ -241,14 +247,22 @@ where
.await; .await;
}); });
self.join = Some(join); self.join = Some(join);
Box::pin(receiver) let stream = stream! {
while let Some(item) = receiver.recv().await {
yield item;
}
};
Box::pin(stream)
} }
} }
pub trait AnyRoomEventExt { pub trait AnyRoomEventExt {
/// Gets the event id of the underlying event
fn event_id(&self) -> &EventId; fn event_id(&self) -> &EventId;
/// Gets the ´origin_server_ts` member of the underlying event /// Gets the ´origin_server_ts` member of the underlying event
fn origin_server_ts(&self) -> SystemTime; fn origin_server_ts(&self) -> SystemTime;
/// Gets the mxc url in a message event if there is noe
fn image_url(&self) -> Option<String>;
} }
impl AnyRoomEventExt for AnyRoomEvent { impl AnyRoomEventExt for AnyRoomEvent {
@ -269,4 +283,26 @@ impl AnyRoomEventExt for AnyRoomEvent {
} }
.to_owned() .to_owned()
} }
fn image_url(&self) -> Option<String> {
match self {
AnyRoomEvent::Message(message) => message.image_url(),
_ => None,
}
}
}
pub trait AnyMessageEventExt {
fn image_url(&self) -> Option<String>;
}
impl AnyMessageEventExt for AnyMessageEvent {
fn image_url(&self) -> Option<String> {
match self {
AnyMessageEvent::RoomMessage(MessageEvent {
content: MessageEventContent::Image(ref image),
..
}) => image.url.clone(),
_ => None,
}
}
} }

347
src/ui.rs
View file

@ -5,22 +5,23 @@ use std::{
use futures::executor::block_on; use futures::executor::block_on;
use iced::{ use iced::{
Application, Button, Column, Command, Container, Element, Length, Row, Rule, Scrollable, Align, Application, Button, Column, Command, Container, Element, Length, Row, Rule, Scrollable,
Subscription, Text, TextInput, Subscription, Text, TextInput,
}; };
use matrix_sdk::{ use matrix_sdk::{
api::r0::message::get_message_events::{ api::r0::{
Request as MessageRequest, Response as MessageResponse, media::get_content::Request as ImageRequest,
message::get_message_events::{Request as MessageRequest, Response as MessageResponse},
}, },
events::{ events::{
key::verification::cancel::CancelCode as VerificationCancelCode, key::verification::cancel::CancelCode as VerificationCancelCode,
room::message::MessageEventContent, AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent, room::{member::MembershipState, message::MessageEventContent},
AnyStateEvent, AnyToDeviceEvent, AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent, AnyStateEvent, AnyToDeviceEvent,
}, },
identifiers::{EventId, RoomAliasId, RoomId, UserId}, identifiers::{EventId, RoomAliasId, RoomId, UserId},
}; };
use crate::matrix::{self, AnyRoomEventExt}; use crate::matrix::{self, AnyMessageEventExt, AnyRoomEventExt};
pub mod prompt; pub mod prompt;
pub mod settings; pub mod settings;
@ -39,31 +40,28 @@ pub enum RoomSorting {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct RoomEntry { pub struct RoomEntry {
/// Cached calculated name /// Cached calculated name
name: String, pub name: String,
/// Room topic /// Room topic
topic: String, pub topic: String,
/// Canonical alias /// Canonical alias
alias: Option<RoomAliasId>, pub alias: Option<RoomAliasId>,
/// Defined display name /// Defined display name
display_name: Option<String>, pub display_name: Option<String>,
/// Person we're in a direct message with /// Person we're in a direct message with
direct: Option<UserId>, pub direct: Option<UserId>,
/// Cache of messages /// Cache of messages
messages: MessageBuffer, pub messages: MessageBuffer,
} }
impl RoomEntry { impl RoomEntry {
/// Recalculate displayname pub fn from_sdk(room: &matrix_sdk::JoinedRoom) -> Self {
pub fn update_display_name(&mut self, id: &RoomId) { Self {
self.name = if let Some(ref name) = self.display_name { direct: room.direct_target(),
name.to_owned() name: block_on(async { room.display_name().await }),
} else if let Some(ref user) = self.direct { topic: room.topic().unwrap_or_default(),
user.to_string() alias: room.canonical_alias(),
} else if let Some(ref alias) = self.alias { ..Default::default()
alias.to_string() }
} else {
id.to_string()
};
} }
} }
@ -82,6 +80,8 @@ pub struct MessageBuffer {
end: Option<String>, end: Option<String>,
/// Most recent activity in the room /// Most recent activity in the room
updated: std::time::SystemTime, updated: std::time::SystemTime,
/// Whether we're awaiting for backfill to be received
loading: bool,
} }
impl MessageBuffer { impl MessageBuffer {
@ -117,6 +117,13 @@ impl MessageBuffer {
self.sort(); self.sort();
self.update_time(); self.update_time();
} }
/// Whather the message buffer has the room creation event
pub fn has_beginning(&self) -> bool {
self.messages
.iter()
.any(|e| matches!(e, AnyRoomEvent::State(AnyStateEvent::RoomCreate(_))))
}
} }
impl Default for MessageBuffer { impl Default for MessageBuffer {
@ -127,6 +134,7 @@ impl Default for MessageBuffer {
start: None, start: None,
end: None, end: None,
updated: SystemTime::UNIX_EPOCH, updated: SystemTime::UNIX_EPOCH,
loading: false,
} }
} }
} }
@ -153,6 +161,8 @@ pub struct MainView {
sorting: RoomSorting, sorting: RoomSorting,
/// Room state /// Room state
rooms: BTreeMap<RoomId, RoomEntry>, rooms: BTreeMap<RoomId, RoomEntry>,
/// A map of mxc urls to image data
images: 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>,
@ -162,6 +172,10 @@ pub struct MainView {
room_scroll: iced::scrollable::State, room_scroll: iced::scrollable::State,
/// Message view scrollbar state /// Message view scrollbar state
message_scroll: iced::scrollable::State, message_scroll: iced::scrollable::State,
/// Backfill fetch button state
backfill_button: iced::button::State,
/// Button to go the room a tombstone points to
tombstone_button: iced::button::State,
/// Message draft text input /// Message draft text input
message_input: iced::text_input::State, message_input: iced::text_input::State,
/// Button to send drafted message /// Button to send drafted message
@ -186,8 +200,11 @@ impl MainView {
sas: None, sas: None,
rooms: Default::default(), rooms: Default::default(),
selected: None, selected: None,
images: Default::default(),
room_scroll: Default::default(), room_scroll: Default::default(),
message_scroll: Default::default(), message_scroll: Default::default(),
backfill_button: Default::default(),
tombstone_button: Default::default(),
message_input: Default::default(), message_input: Default::default(),
dm_buttons: Vec::new(), dm_buttons: Vec::new(),
group_buttons: Vec::new(), group_buttons: Vec::new(),
@ -245,7 +262,12 @@ impl MainView {
.map(|(idx, button)| { .map(|(idx, button)| {
// TODO: highlight selected // TODO: highlight selected
let (id, room) = dm_rooms[idx]; let (id, room) = dm_rooms[idx];
Button::new(button, Text::new(&room.name)) let name = if room.name.is_empty() {
"Missing name"
} else {
&room.name
};
Button::new(button, Text::new(name))
.width(300.into()) .width(300.into())
.on_press(Message::SelectRoom(id.to_owned())) .on_press(Message::SelectRoom(id.to_owned()))
}) })
@ -281,10 +303,17 @@ impl MainView {
let mut message_col = Column::new().spacing(5).padding(5); let mut message_col = Column::new().spacing(5).padding(5);
let selected_room = match self.selected { let selected_room = match self.selected {
Some(ref selected) => self.rooms.get(selected), Some(ref selected) => match (
self.rooms.get(selected),
self.client.get_joined_room(selected),
) {
(Some(room), Some(joined)) => Some((room, joined)),
_ => None,
},
None => None, None => None,
}; };
if let Some(room) = selected_room { if let Some((room, joined)) = selected_room {
// Include user id or canonical alias in title when appropriate
let title = if let Some(ref direct) = room.direct { let title = if let Some(ref direct) = room.direct {
format!("{} ({})", &room.name, direct) format!("{} ({})", &room.name, direct)
} else if let Some(ref alias) = room.alias { } else if let Some(ref alias) = room.alias {
@ -292,29 +321,43 @@ impl MainView {
} else { } else {
room.name.clone() room.name.clone()
}; };
message_col = message_col message_col = message_col
.push(Text::new(title).size(25)) .push(Text::new(title).size(25))
.push(Rule::horizontal(2)); .push(Rule::horizontal(2));
let mut scroll = Scrollable::new(&mut self.message_scroll) let mut scroll = Scrollable::new(&mut self.message_scroll)
.scrollbar_width(2) .scrollbar_width(2)
.height(Length::Fill); .height(Length::Fill);
// Backfill button or loading message
let backfill: Element<_> = if room.messages.loading {
Text::new("Loading...").into()
} else if room.messages.has_beginning() {
let creation = joined.create_content().unwrap();
let mut col =
Column::new().push(Text::new("This is the beginning of room history"));
if let Some(prevous) = creation.predecessor {
col = col.push(
Button::new(&mut self.backfill_button, Text::new("Go to older version"))
.on_press(Message::SelectRoom(prevous.room_id)),
);
}
col.into()
} else {
Button::new(&mut self.backfill_button, Text::new("Load more messages"))
.on_press(Message::BackFill(self.selected.clone().unwrap()))
.into()
};
scroll = scroll.push(Container::new(backfill).width(Length::Fill).center_x());
// Messages
for event in room.messages.messages.iter() { for event in room.messages.messages.iter() {
#[allow(clippy::single_match)] #[allow(clippy::single_match)]
match event { match event {
AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(message)) => { AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(message)) => {
let sender = { let sender =
match self.client.get_joined_room(&message.room_id) { match block_on(async { joined.get_member(&message.sender).await }) {
Some(backend) => { Some(member) => member.name().to_owned(),
match block_on(async {
backend.get_member(&message.sender).await
}) {
Some(member) => member.name().to_owned(),
None => message.sender.to_string(),
}
}
None => message.sender.to_string(), None => message.sender.to_string(),
} };
};
let content: Element<_> = match &message.content { let content: Element<_> = match &message.content {
MessageEventContent::Audio(audio) => { MessageEventContent::Audio(audio) => {
Text::new(format!("Audio message: {}", audio.body)) Text::new(format!("Audio message: {}", audio.body))
@ -323,7 +366,7 @@ impl MainView {
.into() .into()
} }
MessageEventContent::Emote(emote) => { MessageEventContent::Emote(emote) => {
Text::new(format!("{} {}", sender, emote.body)) Text::new(format!("* {} {}", sender, emote.body))
.width(Length::Fill) .width(Length::Fill)
.into() .into()
} }
@ -334,9 +377,24 @@ impl MainView {
.into() .into()
} }
MessageEventContent::Image(image) => { MessageEventContent::Image(image) => {
Text::new(format!("Image with description: {}", image.body)) if let Some(ref url) = image.url {
.width(Length::Fill) match self.images.get(url) {
.into() Some(handle) => Container::new(
iced::Image::new(handle.to_owned())
.width(800.into())
.height(1200.into()),
)
.width(Length::Fill)
.into(),
None => {
Text::new("Image not loaded").width(Length::Fill).into()
}
}
} else {
Text::new("Encrypted images not supported yet")
.width(Length::Fill)
.into()
}
} }
MessageEventContent::Notice(notice) => { MessageEventContent::Notice(notice) => {
Text::new(&notice.body).width(Length::Fill).into() Text::new(&notice.body).width(Length::Fill).into()
@ -367,6 +425,26 @@ impl MainView {
_ => (), _ => (),
} }
} }
// Tombstone
if let Some(tombstone) = joined.tombstone() {
let text = Text::new(format!(
"This room has been upgraded to a new version: {}",
tombstone.body
));
let button =
Button::new(&mut self.tombstone_button, Text::new("Go to upgraded room"))
.on_press(Message::SelectRoom(tombstone.replacement_room));
scroll = scroll.push(
Container::new(
Column::new()
.push(text)
.push(button)
.align_items(Align::Center),
)
.center_x()
.width(Length::Fill),
);
}
message_col = message_col.push(scroll); message_col = message_col.push(scroll);
} else { } else {
message_col = message_col.push( message_col = message_col.push(
@ -490,6 +568,10 @@ pub enum Message {
BackFill(RoomId), BackFill(RoomId),
/// Received backfille /// Received backfille
BackFilled(RoomId, MessageResponse), 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 /// View messages from this room
SelectRoom(RoomId), SelectRoom(RoomId),
/// Set error message /// Set error message
@ -637,48 +719,25 @@ impl Application for Retrix {
*self = Retrix::LoggedIn(MainView::new(client.clone(), session)); *self = Retrix::LoggedIn(MainView::new(client.clone(), session));
let mut commands: Vec<Command<Message>> = Vec::new(); let mut commands: Vec<Command<Message>> = Vec::new();
for room in client.joined_rooms().into_iter() { for room in client.joined_rooms().into_iter() {
let command = async move { //let client = client.clone();
let command: Command<_> = async move {
let room = room.clone(); let room = room.clone();
let entry = RoomEntry { let entry = RoomEntry::from_sdk(&room);
direct: room.direct_target(),
name: block_on(async { room.calculate_name().await }),
topic: room.topic().unwrap_or_default(),
..RoomEntry::default()
};
// Display name calculation for DMs is bronk so we're doing it // Display name calculation for DMs is bronk so we're doing it
// ourselves // ourselves
/*match entry.direct { /*if let Some(ref direct) = entry.direct {
Some(ref direct) => { let request = DisplayNameRequest::new(direct);
let request = matrix_sdk::api::r0::profile::get_display_name::Request::new(direct); if let Ok(response) = client.send(request).await {
if let Ok(response) = client.send(request).await { if let Some(name) = response.displayname {
if let Some(name) = response.displayname { entry.name = name;
entry.name = name;
}
} }
} }
None => entry.name = room.display_name(),
}*/ }*/
/*let messages = room
.messages
.iter()
.cloned()
.map(|event| match event {
AnyPossiblyRedactedSyncMessageEvent::Redacted(e) => {
AnyRoomEvent::RedactedMessage(
e.into_full_event(id.clone()),
)
}
AnyPossiblyRedactedSyncMessageEvent::Regular(e) => {
AnyRoomEvent::Message(e.into_full_event(id.clone()))
}
})
.collect();
entry.messages.messages = messages;*/
Message::ResetRoom(room.room_id().to_owned(), entry) Message::ResetRoom(room.room_id().to_owned(), entry)
} }
.into(); .into();
commands.push(command) commands.push(command);
} }
return Command::batch(commands); return Command::batch(commands);
} }
@ -689,38 +748,57 @@ impl Application for Retrix {
Message::ClearError => view.error = None, Message::ClearError => view.error = None,
Message::SetSort(s) => view.sorting = s, Message::SetSort(s) => view.sorting = s,
Message::ResetRoom(id, room) => { Message::ResetRoom(id, room) => {
view.rooms.insert(id, room).and(Some(())).unwrap_or(()) view.rooms.insert(id.clone(), room);
return async move { Message::BackFill(id) }.into();
} }
Message::SelectRoom(r) => { Message::SelectRoom(r) => {
view.selected = Some(r.clone()); view.selected = Some(r.clone());
return async move { Message::BackFill(r) }.into(); if view.rooms.get(&r).unwrap().messages.messages.is_empty() {
return async move { Message::BackFill(r) }.into();
}
} }
Message::Sync(event) => match event { Message::Sync(event) => match event {
matrix::Event::Room(event) => match event { matrix::Event::Room(event) => 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 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 // 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();
return Command::perform( let marker_cmd = async move {
async move { let result = client
client .read_marker(
.read_marker( event.room_id(),
event.room_id(), event.event_id(),
event.event_id(), Some(event.event_id()),
Some(event.event_id()), )
) .await
.await .err();
.err() match result {
},
|result| match result {
Some(err) => Message::ErrorMessage(err.to_string()), Some(err) => Message::ErrorMessage(err.to_string()),
// TODO: Make this an actual no-op // TODO: Make this an actual no-op
None => Message::Login, None => Message::Login,
}, }
); }
.into();
commands.push(marker_cmd);
} }
return Command::batch(commands);
} }
AnyRoomEvent::State(event) => match event { AnyRoomEvent::State(event) => match event {
AnyStateEvent::RoomCanonicalAlias(ref alias) => { AnyStateEvent::RoomCanonicalAlias(ref alias) => {
@ -731,6 +809,9 @@ impl Application for Retrix {
AnyStateEvent::RoomName(ref name) => { AnyStateEvent::RoomName(ref name) => {
let room = view.rooms.entry(name.room_id.clone()).or_default(); let room = view.rooms.entry(name.room_id.clone()).or_default();
room.display_name = name.content.name().map(String::from); room.display_name = name.content.name().map(String::from);
if let Some(joined) = view.client.get_joined_room(&name.room_id) {
room.name = block_on(async { joined.display_name().await });
}
room.messages.push(AnyRoomEvent::State(event)); room.messages.push(AnyRoomEvent::State(event));
} }
AnyStateEvent::RoomTopic(ref topic) => { AnyStateEvent::RoomTopic(ref topic) => {
@ -738,6 +819,36 @@ impl Application for Retrix {
room.topic = topic.content.topic.clone(); room.topic = topic.content.topic.clone();
room.messages.push(AnyRoomEvent::State(event)); room.messages.push(AnyRoomEvent::State(event));
} }
AnyStateEvent::RoomCreate(ref create) => {
// Add room to the entry list
let room = match view.client.get_joined_room(&create.room_id) {
Some(joined) => view
.rooms
.entry(create.room_id.clone())
.or_insert_with(|| RoomEntry::from_sdk(&joined)),
None => view.rooms.entry(create.room_id.clone()).or_default(),
};
room.messages.push(AnyRoomEvent::State(event));
}
AnyStateEvent::RoomMember(ref member) => {
let room = view.rooms.entry(member.room_id.clone()).or_default();
let client = view.client.clone();
// If we left a room, remove it from the RoomEntry list
let own_id = block_on(async { client.user_id().await })
.unwrap()
.to_string();
if member.content.membership == MembershipState::Leave
&& member.state_key == own_id
{
// Deselect room if we're leaving selected room
if view.selected.as_ref() == Some(&member.room_id) {
view.selected = None;
}
view.rooms.remove(&member.room_id);
return Command::none();
}
room.messages.push(AnyRoomEvent::State(event));
}
ref any => { ref any => {
// Ensure room exists // Ensure room exists
let room = view.rooms.entry(any.room_id().clone()).or_default(); let room = view.rooms.entry(any.room_id().clone()).or_default();
@ -768,7 +879,8 @@ impl Application for Retrix {
} }
}, },
Message::BackFill(id) => { Message::BackFill(id) => {
let room = view.rooms.get(&id).unwrap(); let room = view.rooms.entry(id.clone()).or_default();
room.messages.loading = true;
let client = view.client.clone(); let client = view.client.clone();
let token = match room.messages.end.clone() { let token = match room.messages.end.clone() {
Some(end) => end, Some(end) => end,
@ -789,6 +901,7 @@ impl Application for Retrix {
} }
Message::BackFilled(id, response) => { Message::BackFilled(id, response) => {
let room = view.rooms.get_mut(&id).unwrap(); let room = view.rooms.get_mut(&id).unwrap();
room.messages.loading = false;
let events: Vec<AnyRoomEvent> = response let events: Vec<AnyRoomEvent> = response
.chunk .chunk
.into_iter() .into_iter()
@ -807,7 +920,41 @@ impl Application for Retrix {
if let Some(end) = response.end { if let Some(end) = response.end {
room.messages.end = Some(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); 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()
}
};
println!(
"Getting '{}' from '{}', with url '{}'",
&server, &path, &url
);
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::SetVerification(v) => view.sas = v, Message::SetVerification(v) => view.sas = v,
Message::VerificationAccept => { Message::VerificationAccept => {
@ -965,13 +1112,13 @@ impl Application for Retrix {
} }
fn format_systime(time: std::time::SystemTime) -> String { fn format_systime(time: std::time::SystemTime) -> String {
let secs = time let offset = time::UtcOffset::try_current_local_offset().unwrap_or(time::UtcOffset::UTC);
.duration_since(std::time::SystemTime::UNIX_EPOCH) let time = time::OffsetDateTime::from(time).to_offset(offset);
.unwrap_or_default() let today = time::OffsetDateTime::now_utc().to_offset(offset).date();
.as_secs(); // Display
format!( if time.date() == today {
"{:02}:{:02}", time.format("%T")
(secs % (60 * 60 * 24)) / (60 * 60), } else {
(secs % (60 * 60)) / 60 time.format("%F %T")
) }
} }