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]
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
crossbeam-channel = "0.4"
|
||||
async-stream = "0.3"
|
||||
dirs-next = "2.0"
|
||||
futures = "0.3"
|
||||
iced = { version = "0.2", features = ["debug", "tokio_old", "image"] }
|
||||
iced_futures = "0.2"
|
||||
hostname = "0.3"
|
||||
iced = { git = "https://github.com/hecrj/iced", rev = "31522e3", features = ["debug", "image", "tokio"] }
|
||||
iced_futures = { git = "https://github.com/hecrj/iced", rev = "31522e3" }
|
||||
#iced_glow = { git = "https://github.com/hecrj/iced", rev = "31522e3", features = ["image"] }
|
||||
#matrix-sdk-common-macros = { git = "https://github.com/matrix-org/matrix-rust-sdk", rev = "e65915e" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "0.2", features = ["sync"] }
|
||||
time = "0.2"
|
||||
tokio = { version = "1.0", features = ["sync"] }
|
||||
toml = "0.5"
|
||||
tracing-subscriber = { version = "0.2", features = ["parking_lot"] }
|
||||
|
||||
[dependencies.matrix-sdk]
|
||||
git = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rev = "74998c8"
|
||||
rev = "6435269"
|
||||
default_features = false
|
||||
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;
|
||||
|
||||
use std::fs::Permissions;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
#[cfg(unix)]
|
||||
use std::{fs::Permissions, os::unix::fs::PermissionsExt};
|
||||
|
||||
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.
|
||||
if !config_dir.is_dir() {
|
||||
std::fs::create_dir(&config_dir)?;
|
||||
#[cfg(unix)]
|
||||
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::{
|
||||
api::r0::{account::register::Request as RegistrationRequest, uiaa::AuthData},
|
||||
events::{AnyRoomEvent, AnySyncRoomEvent, AnyToDeviceEvent},
|
||||
identifiers::{DeviceId, EventId, UserId},
|
||||
events::{
|
||||
room::message::{MessageEvent, MessageEventContent},
|
||||
AnyMessageEvent, AnyRoomEvent, AnySyncRoomEvent, AnyToDeviceEvent,
|
||||
},
|
||||
identifiers::{DeviceId, EventId, ServerName, UserId},
|
||||
reqwest::Url,
|
||||
Client, ClientConfig, LoopCtrl, SyncSettings,
|
||||
};
|
||||
@ -67,6 +74,7 @@ pub async fn signup(
|
||||
});
|
||||
|
||||
let response = client.register(request).await?;
|
||||
client.sync_once(SyncSettings::new()).await?;
|
||||
|
||||
let session = Session {
|
||||
access_token: response.access_token.unwrap(),
|
||||
@ -103,7 +111,7 @@ pub async fn login(
|
||||
homeserver: server.to_owned(),
|
||||
};
|
||||
write_session(&session)?;
|
||||
//client.sync_once(SyncSettings::new()).await?;
|
||||
client.sync_once(SyncSettings::new()).await?;
|
||||
|
||||
Ok((client, session))
|
||||
}
|
||||
@ -150,6 +158,19 @@ fn write_session(session: &Session) -> Result<(), Error> {
|
||||
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 {
|
||||
client: matrix_sdk::Client,
|
||||
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)]
|
||||
pub enum Event {
|
||||
Room(AnyRoomEvent),
|
||||
@ -201,13 +207,13 @@ where
|
||||
mut self: Box<Self>,
|
||||
_input: iced_futures::BoxStream<I>,
|
||||
) -> 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 join = tokio::task::spawn(async move {
|
||||
client
|
||||
.sync_with_callback(
|
||||
SyncSettings::new()
|
||||
//.token(client.sync_token().await.unwrap())
|
||||
.token(client.sync_token().await.unwrap())
|
||||
.timeout(Duration::from_secs(90))
|
||||
.full_state(true),
|
||||
|response| async {
|
||||
@ -241,14 +247,22 @@ where
|
||||
.await;
|
||||
});
|
||||
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 {
|
||||
/// Gets the event id of the underlying event
|
||||
fn event_id(&self) -> &EventId;
|
||||
/// Gets the ´origin_server_ts` member of the underlying event
|
||||
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 {
|
||||
@ -269,4 +283,26 @@ impl AnyRoomEventExt for AnyRoomEvent {
|
||||
}
|
||||
.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 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,
|
||||
};
|
||||
use matrix_sdk::{
|
||||
api::r0::message::get_message_events::{
|
||||
Request as MessageRequest, Response as MessageResponse,
|
||||
api::r0::{
|
||||
media::get_content::Request as ImageRequest,
|
||||
message::get_message_events::{Request as MessageRequest, Response as MessageResponse},
|
||||
},
|
||||
events::{
|
||||
key::verification::cancel::CancelCode as VerificationCancelCode,
|
||||
room::message::MessageEventContent, AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent,
|
||||
AnyStateEvent, AnyToDeviceEvent,
|
||||
room::{member::MembershipState, message::MessageEventContent},
|
||||
AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent, AnyStateEvent, AnyToDeviceEvent,
|
||||
},
|
||||
identifiers::{EventId, RoomAliasId, RoomId, UserId},
|
||||
};
|
||||
|
||||
use crate::matrix::{self, AnyRoomEventExt};
|
||||
use crate::matrix::{self, AnyMessageEventExt, AnyRoomEventExt};
|
||||
|
||||
pub mod prompt;
|
||||
pub mod settings;
|
||||
@ -39,31 +40,28 @@ pub enum RoomSorting {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RoomEntry {
|
||||
/// Cached calculated name
|
||||
name: String,
|
||||
pub name: String,
|
||||
/// Room topic
|
||||
topic: String,
|
||||
pub topic: String,
|
||||
/// Canonical alias
|
||||
alias: Option<RoomAliasId>,
|
||||
pub alias: Option<RoomAliasId>,
|
||||
/// Defined display name
|
||||
display_name: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
/// Person we're in a direct message with
|
||||
direct: Option<UserId>,
|
||||
pub direct: Option<UserId>,
|
||||
/// Cache of messages
|
||||
messages: MessageBuffer,
|
||||
pub messages: MessageBuffer,
|
||||
}
|
||||
|
||||
impl RoomEntry {
|
||||
/// Recalculate displayname
|
||||
pub fn update_display_name(&mut self, id: &RoomId) {
|
||||
self.name = if let Some(ref name) = self.display_name {
|
||||
name.to_owned()
|
||||
} else if let Some(ref user) = self.direct {
|
||||
user.to_string()
|
||||
} else if let Some(ref alias) = self.alias {
|
||||
alias.to_string()
|
||||
} else {
|
||||
id.to_string()
|
||||
};
|
||||
pub fn from_sdk(room: &matrix_sdk::JoinedRoom) -> Self {
|
||||
Self {
|
||||
direct: room.direct_target(),
|
||||
name: block_on(async { room.display_name().await }),
|
||||
topic: room.topic().unwrap_or_default(),
|
||||
alias: room.canonical_alias(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +80,8 @@ pub struct MessageBuffer {
|
||||
end: Option<String>,
|
||||
/// Most recent activity in the room
|
||||
updated: std::time::SystemTime,
|
||||
/// Whether we're awaiting for backfill to be received
|
||||
loading: bool,
|
||||
}
|
||||
|
||||
impl MessageBuffer {
|
||||
@ -117,6 +117,13 @@ impl MessageBuffer {
|
||||
self.sort();
|
||||
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 {
|
||||
@ -127,6 +134,7 @@ impl Default for MessageBuffer {
|
||||
start: None,
|
||||
end: None,
|
||||
updated: SystemTime::UNIX_EPOCH,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -153,6 +161,8 @@ pub struct MainView {
|
||||
sorting: RoomSorting,
|
||||
/// Room state
|
||||
rooms: BTreeMap<RoomId, RoomEntry>,
|
||||
/// A map of mxc urls to image data
|
||||
images: BTreeMap<String, iced::image::Handle>,
|
||||
|
||||
/// Room list entries for direct conversations
|
||||
dm_buttons: Vec<iced::button::State>,
|
||||
@ -162,6 +172,10 @@ pub struct MainView {
|
||||
room_scroll: iced::scrollable::State,
|
||||
/// Message view scrollbar 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_input: iced::text_input::State,
|
||||
/// Button to send drafted message
|
||||
@ -186,8 +200,11 @@ impl MainView {
|
||||
sas: None,
|
||||
rooms: Default::default(),
|
||||
selected: None,
|
||||
images: Default::default(),
|
||||
room_scroll: Default::default(),
|
||||
message_scroll: Default::default(),
|
||||
backfill_button: Default::default(),
|
||||
tombstone_button: Default::default(),
|
||||
message_input: Default::default(),
|
||||
dm_buttons: Vec::new(),
|
||||
group_buttons: Vec::new(),
|
||||
@ -245,7 +262,12 @@ impl MainView {
|
||||
.map(|(idx, button)| {
|
||||
// TODO: highlight selected
|
||||
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())
|
||||
.on_press(Message::SelectRoom(id.to_owned()))
|
||||
})
|
||||
@ -281,10 +303,17 @@ impl MainView {
|
||||
|
||||
let mut message_col = Column::new().spacing(5).padding(5);
|
||||
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,
|
||||
};
|
||||
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 {
|
||||
format!("{} ({})", &room.name, direct)
|
||||
} else if let Some(ref alias) = room.alias {
|
||||
@ -292,28 +321,42 @@ impl MainView {
|
||||
} else {
|
||||
room.name.clone()
|
||||
};
|
||||
|
||||
message_col = message_col
|
||||
.push(Text::new(title).size(25))
|
||||
.push(Rule::horizontal(2));
|
||||
let mut scroll = Scrollable::new(&mut self.message_scroll)
|
||||
.scrollbar_width(2)
|
||||
.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() {
|
||||
#[allow(clippy::single_match)]
|
||||
match event {
|
||||
AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(message)) => {
|
||||
let sender = {
|
||||
match self.client.get_joined_room(&message.room_id) {
|
||||
Some(backend) => {
|
||||
match block_on(async {
|
||||
backend.get_member(&message.sender).await
|
||||
}) {
|
||||
let sender =
|
||||
match block_on(async { joined.get_member(&message.sender).await }) {
|
||||
Some(member) => member.name().to_owned(),
|
||||
None => message.sender.to_string(),
|
||||
}
|
||||
}
|
||||
None => message.sender.to_string(),
|
||||
}
|
||||
};
|
||||
let content: Element<_> = match &message.content {
|
||||
MessageEventContent::Audio(audio) => {
|
||||
@ -323,7 +366,7 @@ impl MainView {
|
||||
.into()
|
||||
}
|
||||
MessageEventContent::Emote(emote) => {
|
||||
Text::new(format!("{} {}", sender, emote.body))
|
||||
Text::new(format!("* {} {}", sender, emote.body))
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
@ -334,10 +377,25 @@ impl MainView {
|
||||
.into()
|
||||
}
|
||||
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)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
MessageEventContent::Notice(notice) => {
|
||||
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);
|
||||
} else {
|
||||
message_col = message_col.push(
|
||||
@ -490,6 +568,10 @@ pub enum Message {
|
||||
BackFill(RoomId),
|
||||
/// Received backfille
|
||||
BackFilled(RoomId, MessageResponse),
|
||||
/// Fetch an image pointed to by an mxc url
|
||||
FetchImage(String),
|
||||
/// Fetched an image
|
||||
FetchedImage(String, iced::image::Handle),
|
||||
/// View messages from this room
|
||||
SelectRoom(RoomId),
|
||||
/// Set error message
|
||||
@ -637,48 +719,25 @@ impl Application for Retrix {
|
||||
*self = Retrix::LoggedIn(MainView::new(client.clone(), session));
|
||||
let mut commands: Vec<Command<Message>> = Vec::new();
|
||||
for room in client.joined_rooms().into_iter() {
|
||||
let command = async move {
|
||||
//let client = client.clone();
|
||||
let command: Command<_> = async move {
|
||||
let room = room.clone();
|
||||
let entry = RoomEntry {
|
||||
direct: room.direct_target(),
|
||||
name: block_on(async { room.calculate_name().await }),
|
||||
topic: room.topic().unwrap_or_default(),
|
||||
..RoomEntry::default()
|
||||
};
|
||||
let entry = RoomEntry::from_sdk(&room);
|
||||
|
||||
// Display name calculation for DMs is bronk so we're doing it
|
||||
// ourselves
|
||||
/*match entry.direct {
|
||||
Some(ref direct) => {
|
||||
let request = matrix_sdk::api::r0::profile::get_display_name::Request::new(direct);
|
||||
/*if let Some(ref direct) = entry.direct {
|
||||
let request = DisplayNameRequest::new(direct);
|
||||
if let Ok(response) = client.send(request).await {
|
||||
if let Some(name) = response.displayname {
|
||||
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)
|
||||
}
|
||||
.into();
|
||||
commands.push(command)
|
||||
commands.push(command);
|
||||
}
|
||||
return Command::batch(commands);
|
||||
}
|
||||
@ -689,39 +748,58 @@ impl Application for Retrix {
|
||||
Message::ClearError => view.error = None,
|
||||
Message::SetSort(s) => view.sorting = s,
|
||||
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) => {
|
||||
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();
|
||||
return Command::perform(
|
||||
async move {
|
||||
client
|
||||
let marker_cmd = async move {
|
||||
let result = client
|
||||
.read_marker(
|
||||
event.room_id(),
|
||||
event.event_id(),
|
||||
Some(event.event_id()),
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
},
|
||||
|result| match result {
|
||||
.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();
|
||||
@ -731,6 +809,9 @@ impl Application for Retrix {
|
||||
AnyStateEvent::RoomName(ref name) => {
|
||||
let room = view.rooms.entry(name.room_id.clone()).or_default();
|
||||
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));
|
||||
}
|
||||
AnyStateEvent::RoomTopic(ref topic) => {
|
||||
@ -738,6 +819,36 @@ impl Application for Retrix {
|
||||
room.topic = topic.content.topic.clone();
|
||||
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 => {
|
||||
// Ensure room exists
|
||||
let room = view.rooms.entry(any.room_id().clone()).or_default();
|
||||
@ -768,7 +879,8 @@ impl Application for Retrix {
|
||||
}
|
||||
},
|
||||
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 token = match room.messages.end.clone() {
|
||||
Some(end) => end,
|
||||
@ -789,6 +901,7 @@ impl Application for Retrix {
|
||||
}
|
||||
Message::BackFilled(id, response) => {
|
||||
let room = view.rooms.get_mut(&id).unwrap();
|
||||
room.messages.loading = false;
|
||||
let events: Vec<AnyRoomEvent> = response
|
||||
.chunk
|
||||
.into_iter()
|
||||
@ -807,7 +920,41 @@ impl Application for Retrix {
|
||||
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()
|
||||
}
|
||||
};
|
||||
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::VerificationAccept => {
|
||||
@ -965,13 +1112,13 @@ impl Application for Retrix {
|
||||
}
|
||||
|
||||
fn format_systime(time: std::time::SystemTime) -> String {
|
||||
let secs = time
|
||||
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
format!(
|
||||
"{:02}:{:02}",
|
||||
(secs % (60 * 60 * 24)) / (60 * 60),
|
||||
(secs % (60 * 60)) / 60
|
||||
)
|
||||
let offset = time::UtcOffset::try_current_local_offset().unwrap_or(time::UtcOffset::UTC);
|
||||
let time = time::OffsetDateTime::from(time).to_offset(offset);
|
||||
let today = time::OffsetDateTime::now_utc().to_offset(offset).date();
|
||||
// Display
|
||||
if time.date() == today {
|
||||
time.format("%T")
|
||||
} else {
|
||||
time.format("%F %T")
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user