Intial image support, proper time formatting
Also feature gate unix exclusive code
This commit is contained in:
parent
2a9dbb90d3
commit
f25d3b6821
14
Cargo.toml
14
Cargo.toml
@ -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"]
|
||||||
|
|
||||||
|
@ -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))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
319
src/ui.rs
319
src/ui.rs
@ -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,28 +321,42 @@ 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) => {
|
|
||||||
match block_on(async {
|
|
||||||
backend.get_member(&message.sender).await
|
|
||||||
}) {
|
|
||||||
Some(member) => member.name().to_owned(),
|
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) => {
|
||||||
@ -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,10 +377,25 @@ 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 {
|
||||||
|
match self.images.get(url) {
|
||||||
|
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)
|
.width(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
MessageEventContent::Notice(notice) => {
|
MessageEventContent::Notice(notice) => {
|
||||||
Text::new(¬ice.body).width(Length::Fill).into()
|
Text::new(¬ice.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,39 +748,58 @@ 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());
|
||||||
|
if view.rooms.get(&r).unwrap().messages.messages.is_empty() {
|
||||||
return async move { Message::BackFill(r) }.into();
|
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) => {
|
||||||
let room = view.rooms.entry(alias.room_id.clone()).or_default();
|
let room = view.rooms.entry(alias.room_id.clone()).or_default();
|
||||||
@ -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")
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user