diff --git a/src/prelude.rs b/src/prelude.rs index e9c522d6..5b22279d 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1 +1,4 @@ -pub use crate::session::UserExt; +pub use crate::session::{ + room::{EventExt, TimelineItemExt}, + UserExt, +}; diff --git a/src/session/content/room_history/item_row.rs b/src/session/content/room_history/item_row.rs index cd32f29e..2711d10c 100644 --- a/src/session/content/room_history/item_row.rs +++ b/src/session/content/room_history/item_row.rs @@ -5,11 +5,12 @@ use matrix_sdk::ruma::events::AnySyncRoomEvent; use crate::{ components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl, ReactionChooser}, + prelude::*, session::{ content::room_history::{message_row::MessageRow, DividerRow, RoomHistory, StateRow}, room::{ - Event, EventActions, TimelineDayDivider, TimelineItem, TimelineNewMessagesDivider, - TimelineSpinner, + Event, EventActions, SupportedEvent, TimelineDayDivider, TimelineItem, + TimelineNewMessagesDivider, TimelineSpinner, }, }, }; @@ -95,7 +96,7 @@ mod imp { .item .borrow() .as_ref() - .and_then(|item| item.downcast_ref::()) + .and_then(|item| item.downcast_ref::()) { if let Some(handler) = self.notify_handler.borrow_mut().take() { event.disconnect(handler); @@ -113,7 +114,10 @@ mod imp { let room_history = obj.room_history(); let popover = room_history.item_context_menu().to_owned(); - if event.content().is_some() { + if let Some(event) = event + .downcast_ref::() + .filter(|event| event.content().is_some()) + { let menu_model = Self::Type::event_message_menu_model(); let reaction_chooser = room_history.item_reaction_chooser(); if popover.menu_model().as_ref() != Some(menu_model) { @@ -189,7 +193,7 @@ impl ItemRow { .item .borrow() .as_ref() - .and_then(|item| item.downcast_ref::()) + .and_then(|item| item.downcast_ref::()) { if let Some(handler) = priv_.notify_handler.borrow_mut().take() { event.disconnect(handler); @@ -199,15 +203,13 @@ impl ItemRow { } if let Some(ref item) = item { - if let Some(event) = item.downcast_ref::() { - self.set_action_group(self.set_event_actions(Some(event))); + if let Some(event) = item.downcast_ref::() { + self.set_action_group(self.set_event_actions(Some(event.upcast_ref()))); - let notify_handler = event.connect_notify_local( - Some("event"), - clone!(@weak self as obj => move |event, _| { + let notify_handler = + event.connect_pure_event_notify(clone!(@weak self as obj => move |event| { obj.set_event_widget(event); - }), - ); + })); priv_.notify_handler.replace(Some(notify_handler)); self.set_event_widget(event); @@ -265,9 +267,9 @@ impl ItemRow { priv_.item.replace(item); } - fn set_event_widget(&self, event: &Event) { + fn set_event_widget(&self, event: &SupportedEvent) { match event.matrix_event() { - Some(AnySyncRoomEvent::State(state)) => { + AnySyncRoomEvent::State(state) => { let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::()) { child diff --git a/src/session/content/room_history/message_row/mod.rs b/src/session/content/room_history/message_row/mod.rs index 2804a401..8668aaf2 100644 --- a/src/session/content/room_history/message_row/mod.rs +++ b/src/session/content/room_history/message_row/mod.rs @@ -25,7 +25,7 @@ use self::{ reaction_list::MessageReactionList, reply::MessageReply, text::MessageText, }; use crate::{ - components::Avatar, prelude::*, session::room::Event, spawn, utils::filename_for_mime, + components::Avatar, prelude::*, session::room::SupportedEvent, spawn, utils::filename_for_mime, }; mod imp { @@ -53,7 +53,7 @@ mod imp { pub reactions: TemplateChild, pub source_changed_handler: RefCell>, pub bindings: RefCell>, - pub event: RefCell>, + pub event: RefCell>, } #[glib::object_subclass] @@ -144,7 +144,7 @@ impl MessageRow { self.notify("show-header"); } - pub fn set_event(&self, event: Event) { + pub fn set_event(&self, event: SupportedEvent) { let priv_ = self.imp(); // Remove signals and bindings from the previous event if let Some(event) = priv_.event.take() { @@ -195,14 +195,20 @@ impl MessageRow { priv_.event.replace(Some(event)); } - fn update_content(&self, event: &Event) { + fn update_content(&self, event: &SupportedEvent) { if event.is_reply() { spawn!( glib::PRIORITY_HIGH, clone!(@weak self as obj, @weak event => async move { let priv_ = obj.imp(); - if let Ok(Some(related_event)) = event.reply_to_event().await { + if let Some(related_event) = event + .reply_to_event() + .await + .ok() + .flatten() + .and_then(|event| event.downcast::().ok()) + { let reply = MessageReply::new(); reply.set_related_content_sender(related_event.sender().upcast()); build_content(reply.related_content(), &related_event, true); @@ -229,7 +235,7 @@ impl Default for MessageRow { /// /// If `compact` is true, the content should appear in a smaller format without /// interactions, if possible. -fn build_content(parent: &adw::Bin, event: &Event, compact: bool) { +fn build_content(parent: &adw::Bin, event: &SupportedEvent, compact: bool) { match event.content() { Some(AnyMessageLikeEventContent::RoomMessage(message)) => { let msgtype = if let Some(Relation::Replacement(replacement)) = message.relates_to { diff --git a/src/session/content/room_history/mod.rs b/src/session/content/room_history/mod.rs index d2d0ca33..a4e72a43 100644 --- a/src/session/content/room_history/mod.rs +++ b/src/session/content/room_history/mod.rs @@ -37,7 +37,7 @@ use crate::{ i18n::gettext_f, session::{ content::{MarkdownPopover, RoomDetails}, - room::{Event, Room, RoomType, Timeline, TimelineItem, TimelineState}, + room::{Room, RoomType, SupportedEvent, Timeline, TimelineItem, TimelineState}, user::UserExt, }, spawn, toast, @@ -280,7 +280,7 @@ mod imp { .model() .and_then(|model| model.item(pos)) .as_ref() - .and_then(|o| o.downcast_ref::()) + .and_then(|o| o.downcast_ref::()) { if let Some(room) = obj.room() { room.session().show_media(event); diff --git a/src/session/media_viewer.rs b/src/session/media_viewer.rs index 4a5bdce3..3f67d556 100644 --- a/src/session/media_viewer.rs +++ b/src/session/media_viewer.rs @@ -6,7 +6,7 @@ use matrix_sdk::ruma::events::{room::message::MessageType, AnyMessageLikeEventCo use super::room::EventActions; use crate::{ components::{ContentType, MediaContentViewer}, - session::room::Event, + session::room::SupportedEvent, spawn, utils::cache_dir, Window, @@ -24,7 +24,7 @@ mod imp { #[template(resource = "/org/gnome/Fractal/media-viewer.ui")] pub struct MediaViewer { pub fullscreened: Cell, - pub event: RefCell>>, + pub event: RefCell>>, pub body: RefCell>, #[template_child] pub flap: TemplateChild, @@ -91,7 +91,7 @@ mod imp { "event", "Event", "The media event to display", - Event::static_type(), + SupportedEvent::static_type(), glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, ), glib::ParamSpecString::new( @@ -164,7 +164,7 @@ impl MediaViewer { glib::Object::new(&[]).expect("Failed to create MediaViewer") } - pub fn event(&self) -> Option { + pub fn event(&self) -> Option { self.imp() .event .borrow() @@ -172,7 +172,7 @@ impl MediaViewer { .and_then(|event| event.upgrade()) } - pub fn set_event(&self, event: Option) { + pub fn set_event(&self, event: Option) { if event == self.event() { return; } @@ -226,7 +226,7 @@ impl MediaViewer { self.imp().media.show_loading(); if let Some(event) = self.event() { - self.set_event_actions(Some(&event)); + self.set_event_actions(Some(event.upcast_ref())); if let Some(AnyMessageLikeEventContent::RoomMessage(content)) = event.content() { match content.msgtype { MessageType::Image(image) => { diff --git a/src/session/mod.rs b/src/session/mod.rs index b3e17d58..d26c049b 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -63,7 +63,7 @@ use self::{ }; pub use self::{ avatar::Avatar, - room::{Event, Room}, + room::{Room, SupportedEvent}, room_creation::RoomCreation, user::{User, UserActions, UserExt}, }; @@ -896,7 +896,7 @@ impl Session { } /// Show a media event - pub fn show_media(&self, event: &Event) { + pub fn show_media(&self, event: &SupportedEvent) { let priv_ = self.imp(); priv_.media_viewer.set_event(Some(event.clone())); diff --git a/src/session/room/event/mod.rs b/src/session/room/event/mod.rs new file mode 100644 index 00000000..004cc344 --- /dev/null +++ b/src/session/room/event/mod.rs @@ -0,0 +1,419 @@ +use gtk::{glib, prelude::*, subclass::prelude::*}; +use log::warn; +use matrix_sdk::{ + deserialized_responses::SyncRoomEvent, + ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId}, +}; + +use super::{ + timeline::{TimelineItem, TimelineItemImpl}, + Member, Room, +}; + +mod supported_event; +mod unsupported_event; + +pub use supported_event::SupportedEvent; +pub use unsupported_event::UnsupportedEvent; + +#[derive(Clone, Debug, glib::Boxed)] +#[boxed_type(name = "BoxedSyncRoomEvent")] +pub struct BoxedSyncRoomEvent(SyncRoomEvent); + +mod imp { + use std::cell::RefCell; + + use glib::{object::WeakRef, Class}; + use once_cell::{sync::Lazy, unsync::OnceCell}; + + use super::*; + + #[repr(C)] + pub struct EventClass { + pub parent_class: Class, + pub source: fn(&super::Event) -> String, + pub event_id: fn(&super::Event) -> Option, + pub sender_id: fn(&super::Event) -> Option, + pub origin_server_ts: fn(&super::Event) -> Option, + } + + unsafe impl ClassStruct for EventClass { + type Type = Event; + } + + pub(super) fn event_source(this: &super::Event) -> String { + let klass = this.class(); + (klass.as_ref().source)(this) + } + + pub(super) fn event_event_id(this: &super::Event) -> Option { + let klass = this.class(); + (klass.as_ref().event_id)(this) + } + + pub(super) fn event_sender_id(this: &super::Event) -> Option { + let klass = this.class(); + (klass.as_ref().sender_id)(this) + } + + pub(super) fn event_origin_server_ts( + this: &super::Event, + ) -> Option { + let klass = this.class(); + (klass.as_ref().origin_server_ts)(this) + } + + #[derive(Debug, Default)] + pub struct Event { + /// The SDK event containing encryption information and the serialized + /// event as `Raw`. + pub pure_event: RefCell>, + + /// The room containing this `Event`. + pub room: OnceCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for Event { + const NAME: &'static str = "RoomEvent"; + const ABSTRACT: bool = true; + type Type = super::Event; + type ParentType = TimelineItem; + type Class = EventClass; + } + + impl ObjectImpl for Event { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + glib::ParamSpecBoxed::new( + "pure-event", + "Pure Event", + "The pure Matrix event of this Event", + BoxedSyncRoomEvent::static_type(), + glib::ParamFlags::WRITABLE, + ), + glib::ParamSpecString::new( + "source", + "Source", + "The JSON source of this Event", + None, + glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY, + ), + glib::ParamSpecObject::new( + "room", + "Room", + "The room containing this Event", + Room::static_type(), + glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY, + ), + glib::ParamSpecString::new( + "time", + "Time", + "The locally formatted time of this Matrix event", + None, + glib::ParamFlags::READABLE, + ), + ] + }); + + PROPERTIES.as_ref() + } + + fn set_property( + &self, + obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "pure-event" => { + let event = value.get::().unwrap(); + obj.set_pure_event(event.0); + } + "room" => { + self.room + .set(value.get::().unwrap().downgrade()) + .unwrap(); + } + _ => unimplemented!(), + } + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "source" => obj.source().to_value(), + "room" => obj.room().to_value(), + "time" => obj.time().to_value(), + _ => unimplemented!(), + } + } + } + + impl TimelineItemImpl for Event { + fn event_sender(&self, obj: &Self::Type) -> Option { + Some(obj.room().members().member_by_id(obj.sender_id()?)) + } + + fn selectable(&self, _obj: &Self::Type) -> bool { + true + } + } +} + +glib::wrapper! { + /// GObject representation of a Matrix room event. + pub struct Event(ObjectSubclass) @extends TimelineItem; +} + +impl Event { + /// Create an `Event` with the given pure SDK event and room. + /// + /// Constructs the proper subtype according to the event. + pub fn new(pure_event: SyncRoomEvent, room: &Room) -> Self { + SupportedEvent::try_from_event(pure_event.clone(), room) + .map(|event| event.upcast()) + .unwrap_or_else(|_| { + warn!("Failed to deserialize event: {:?}", pure_event); + UnsupportedEvent::new(pure_event, room).upcast() + }) + } +} + +/// Public trait containing implemented methods for everything that derives from +/// `Event`. +/// +/// To override the behavior of these methods, override the corresponding method +/// of `EventImpl`. +pub trait EventExt: 'static { + /// The `Room` where this `Event` was sent. + fn room(&self) -> Room; + + /// The pure SDK event of this `Event`. + fn pure_event(&self) -> SyncRoomEvent; + + /// Set the pure SDK event of this `Event`. + fn set_pure_event(&self, pure_event: SyncRoomEvent); + + /// The source JSON of this `Event`. + fn original_source(&self) -> String; + + /// The source JSON displayed for this `Event`. + /// + /// Defaults to the `original_source`. + fn source(&self) -> String; + + /// The event ID of this `Event`, if it was found. + fn event_id(&self) -> Option; + + /// The user ID of the sender of this `Event`, if it was found. + fn sender_id(&self) -> Option; + + /// The timestamp on the origin server when this `Event` was sent as + /// `MilliSecondsSinceUnixEpoch`, if it was found. + fn origin_server_ts(&self) -> Option; + + /// The timestamp on the origin server when this `Event` was sent as + /// `glib::DateTime`. + /// + /// This is computed from the `origin_server_ts`. + fn timestamp(&self) -> Option { + glib::DateTime::from_unix_utc(self.origin_server_ts()?.as_secs().into()) + .and_then(|t| t.to_local()) + .ok() + } + + /// The formatted time when this `Event` was sent. + /// + /// This is computed from the `origin_server_ts`. + fn time(&self) -> Option { + let datetime = self.timestamp()?; + + // FIXME Is there a cleaner to find out if we should use 24h format? + let local_time = datetime.format("%X").unwrap().as_str().to_ascii_lowercase(); + + let time = if local_time.ends_with("am") || local_time.ends_with("pm") { + // Use 12h time format (AM/PM) + datetime.format("%l∶%M %p").unwrap().to_string() + } else { + // Use 24 time format + datetime.format("%R").unwrap().to_string() + }; + Some(time) + } + + fn connect_pure_event_notify(&self, f: F) -> glib::SignalHandlerId; +} + +impl> EventExt for O { + fn room(&self) -> Room { + self.upcast_ref() + .imp() + .room + .get() + .unwrap() + .upgrade() + .unwrap() + } + + fn pure_event(&self) -> SyncRoomEvent { + self.upcast_ref().imp().pure_event.borrow().clone().unwrap() + } + + fn set_pure_event(&self, pure_event: SyncRoomEvent) { + let priv_ = self.upcast_ref().imp(); + priv_.pure_event.replace(Some(pure_event)); + + self.notify("pure-event"); + self.notify("source"); + } + + fn original_source(&self) -> String { + let pure_event = self.upcast_ref().imp().pure_event.borrow(); + let raw = pure_event.as_ref().unwrap().event.json().get(); + + // We have to convert it to a Value, because a RawValue cannot be + // pretty-printed. + if let Ok(json) = serde_json::from_str::(raw) { + serde_json::to_string_pretty(&json).unwrap() + } else { + raw.to_owned() + } + } + + fn source(&self) -> String { + imp::event_source(self.upcast_ref()) + } + + fn event_id(&self) -> Option { + imp::event_event_id(self.upcast_ref()) + } + + fn sender_id(&self) -> Option { + imp::event_sender_id(self.upcast_ref()) + } + + fn origin_server_ts(&self) -> Option { + imp::event_origin_server_ts(self.upcast_ref()) + } + + fn connect_pure_event_notify(&self, f: F) -> glib::SignalHandlerId { + self.connect_notify_local(Some("pure-event"), move |this, _| { + f(this); + }) + } +} + +/// Public trait that must be implemented for everything that derives from +/// `Event`. +/// +/// Overriding a method from this trait overrides also its behavior in +/// `EventExt`. +pub trait EventImpl: ObjectImpl { + fn source(&self, obj: &Self::Type) -> String { + obj.dynamic_cast_ref::() + .map(|event| event.original_source()) + .unwrap_or_default() + } + + fn event_id(&self, obj: &Self::Type) -> Option { + obj.dynamic_cast_ref::().and_then(|event| { + event + .imp() + .pure_event + .borrow() + .as_ref() + .unwrap() + .event + .get_field::("event_id") + .ok() + .flatten() + }) + } + + fn sender_id(&self, obj: &Self::Type) -> Option { + obj.dynamic_cast_ref::().and_then(|event| { + event + .imp() + .pure_event + .borrow() + .as_ref() + .unwrap() + .event + .get_field::("sender") + .ok() + .flatten() + }) + } + + fn origin_server_ts(&self, obj: &Self::Type) -> Option { + obj.dynamic_cast_ref::().and_then(|event| { + event + .imp() + .pure_event + .borrow() + .as_ref() + .unwrap() + .event + .get_field::("origin_server_ts") + .ok() + .flatten() + }) + } +} + +// Make `Event` subclassable. +unsafe impl IsSubclassable for Event +where + T: TimelineItemImpl + EventImpl, + T::Type: IsA + IsA, +{ + fn class_init(class: &mut glib::Class) { + Self::parent_class_init::(class.upcast_ref_mut()); + + let klass = class.as_mut(); + + klass.source = source_trampoline::; + klass.event_id = event_id_trampoline::; + klass.sender_id = sender_id_trampoline::; + klass.origin_server_ts = origin_server_ts_trampoline::; + } +} + +// Virtual method implementation trampolines. +fn source_trampoline(this: &Event) -> String +where + T: ObjectSubclass + EventImpl, + T::Type: IsA, +{ + let this = this.downcast_ref::().unwrap(); + this.imp().source(this) +} + +fn event_id_trampoline(this: &Event) -> Option +where + T: ObjectSubclass + EventImpl, + T::Type: IsA, +{ + let this = this.downcast_ref::().unwrap(); + this.imp().event_id(this) +} + +fn sender_id_trampoline(this: &Event) -> Option +where + T: ObjectSubclass + EventImpl, + T::Type: IsA, +{ + let this = this.downcast_ref::().unwrap(); + this.imp().sender_id(this) +} + +fn origin_server_ts_trampoline(this: &Event) -> Option +where + T: ObjectSubclass + EventImpl, + T::Type: IsA, +{ + let this = this.downcast_ref::().unwrap(); + this.imp().origin_server_ts(this) +} diff --git a/src/session/room/event.rs b/src/session/room/event/supported_event.rs similarity index 58% rename from src/session/room/event.rs rename to src/session/room/event/supported_event.rs index 5d91b133..e5496831 100644 --- a/src/session/room/event.rs +++ b/src/session/room/event/supported_event.rs @@ -1,9 +1,4 @@ -use gtk::{ - glib, - glib::{clone, DateTime}, - prelude::*, - subclass::prelude::*, -}; +use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*}; use log::warn; use matrix_sdk::{ deserialized_responses::SyncRoomEvent, @@ -16,89 +11,67 @@ use matrix_sdk::{ redaction::SyncRoomRedactionEvent, }, AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncRoomEvent, - AnySyncStateEvent, MessageLikeUnsigned, SyncMessageLikeEvent, SyncStateEvent, + AnySyncStateEvent, SyncMessageLikeEvent, SyncStateEvent, }, serde::Raw, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, }, Error as MatrixError, }; +use serde_json::Error as JsonError; -use super::{ - timeline::{TimelineItem, TimelineItemImpl}, - Member, ReactionList, Room, -}; +use super::{BoxedSyncRoomEvent, Event, EventImpl}; use crate::{ + prelude::*, + session::room::{ + timeline::{TimelineItem, TimelineItemImpl}, + Member, ReactionList, Room, + }, spawn, spawn_tokio, utils::{filename_for_mime, media_type_uid}, }; #[derive(Clone, Debug, glib::Boxed)] -#[boxed_type(name = "BoxedSyncRoomEvent")] -pub struct BoxedSyncRoomEvent(SyncRoomEvent); +#[boxed_type(name = "BoxedAnySyncRoomEvent")] +pub struct BoxedAnySyncRoomEvent(AnySyncRoomEvent); mod imp { use std::cell::RefCell; - use glib::{object::WeakRef, SignalHandlerId}; - use once_cell::{sync::Lazy, unsync::OnceCell}; + use glib::SignalHandlerId; + use once_cell::sync::Lazy; use super::*; #[derive(Debug, Default)] - pub struct Event { - /// The deserialized matrix event - pub event: RefCell>, - /// The SDK event containing encryption information and the serialized - /// event as `Raw` - pub pure_event: RefCell>, + pub struct SupportedEvent { + /// The deserialized Matrix event. + pub matrix_event: RefCell>, /// Events that replace this one, in the order they arrive. - pub replacing_events: RefCell>, + pub replacing_events: RefCell>, pub reactions: ReactionList, - pub source_changed_handler: RefCell>, pub keys_handle: RefCell>, - pub room: OnceCell>, + pub source_changed_handler: RefCell>, } #[glib::object_subclass] - impl ObjectSubclass for Event { - const NAME: &'static str = "RoomEvent"; - type Type = super::Event; - type ParentType = TimelineItem; + impl ObjectSubclass for SupportedEvent { + const NAME: &'static str = "RoomSupportedEvent"; + type Type = super::SupportedEvent; + type ParentType = Event; } - impl ObjectImpl for Event { + impl ObjectImpl for SupportedEvent { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { vec![ glib::ParamSpecBoxed::new( - "event", - "event", - "The matrix event of this Event", - BoxedSyncRoomEvent::static_type(), + "matrix-event", + "Matrix Event", + "The deserialized Matrix event of this Event", + BoxedAnySyncRoomEvent::static_type(), glib::ParamFlags::WRITABLE, ), - glib::ParamSpecString::new( - "source", - "Source", - "The source (JSON) of this Event", - None, - glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY, - ), - glib::ParamSpecObject::new( - "room", - "Room", - "The room containing this event", - Room::static_type(), - glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY, - ), - glib::ParamSpecString::new( - "time", - "Time", - "The locally formatted time of this matrix event", - None, - glib::ParamFlags::READABLE, - ), glib::ParamSpecObject::new( "reactions", "Reactions", @@ -120,14 +93,9 @@ mod imp { pspec: &glib::ParamSpec, ) { match pspec.name() { - "event" => { - let event = value.get::().unwrap(); - obj.set_matrix_pure_event(event.0); - } - "room" => { - self.room - .set(value.get::().unwrap().downgrade()) - .unwrap(); + "matrix-event" => { + let matrix_event = value.get::().unwrap(); + obj.set_matrix_event(matrix_event.0); } _ => unimplemented!(), } @@ -135,20 +103,13 @@ mod imp { fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { match pspec.name() { - "source" => obj.source().to_value(), - "room" => obj.room().to_value(), - "time" => obj.time().to_value(), "reactions" => obj.reactions().to_value(), _ => unimplemented!(), } } } - impl TimelineItemImpl for Event { - fn selectable(&self, _obj: &Self::Type) -> bool { - true - } - + impl TimelineItemImpl for SupportedEvent { fn activatable(&self, obj: &Self::Type) -> bool { match obj.original_content() { // The event can be activated to open the media viewer if it's an image or a video. @@ -181,72 +142,79 @@ mod imp { } } - fn sender(&self, obj: &Self::Type) -> Option { - Some(obj.room().members().member_by_id(obj.matrix_sender())) + fn event_sender(&self, obj: &Self::Type) -> Option { + Some(obj.sender()) + } + } + + impl EventImpl for SupportedEvent { + fn source(&self, obj: &Self::Type) -> String { + obj.replacement() + .map(|replacement| replacement.source()) + .unwrap_or_else(|| obj.original_source()) + } + + fn origin_server_ts(&self, _obj: &Self::Type) -> Option { + Some( + self.matrix_event + .borrow() + .as_ref() + .unwrap() + .origin_server_ts(), + ) } } } glib::wrapper! { - /// GObject representation of a Matrix room event. - pub struct Event(ObjectSubclass) @extends TimelineItem; + /// GObject representation of a supported Matrix room event. + pub struct SupportedEvent(ObjectSubclass) @extends TimelineItem, Event; } // TODO: -// - [ ] implement operations for events: forward, reply, delete... - -impl Event { - pub fn new(event: SyncRoomEvent, room: &Room) -> Self { - let event = BoxedSyncRoomEvent(event); - glib::Object::new(&[("event", &event), ("room", room)]).expect("Failed to create Event") - } - - pub fn sender(&self) -> Member { - self.room().members().member_by_id(self.matrix_sender()) - } - - pub fn room(&self) -> Room { - self.imp().room.get().unwrap().upgrade().unwrap() - } +// - [ ] implement operations for events: forward, reply, edit... - /// Get the matrix event +impl SupportedEvent { + /// Try to construct a new `SupportedEvent` with the given pure event and + /// room. /// - /// If the `SyncRoomEvent` couldn't be deserialized this is `None` - pub fn matrix_event(&self) -> Option { - self.imp().event.borrow().clone() - } - - pub fn matrix_pure_event(&self) -> SyncRoomEvent { - self.imp().pure_event.borrow().clone().unwrap() - } - - pub fn set_matrix_pure_event(&self, event: SyncRoomEvent) { - let priv_ = self.imp(); - - if let Ok(deserialized) = event.event.deserialize() { - if let AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted( - SyncMessageLikeEvent::Original(_), - )) = deserialized - { - let raw_event = event.event.clone(); - spawn!(clone!(@weak self as obj => async move { - obj.try_to_decrypt(raw_event.cast()).await; - })); - } - - priv_.event.replace(Some(deserialized)); - } else { - warn!("Failed to deserialize event: {:?}", event); + /// Returns an error if the pure event fails to deserialize. + pub fn try_from_event(pure_event: SyncRoomEvent, room: &Room) -> Result { + let matrix_event = BoxedAnySyncRoomEvent(pure_event.event.deserialize()?); + let pure_event = BoxedSyncRoomEvent(pure_event); + Ok(glib::Object::new(&[ + ("pure-event", &pure_event), + ("matrix-event", &matrix_event), + ("room", room), + ]) + .expect("Failed to create SupportedEvent")) + } + + /// Set the deserialized Matrix event of this `SupportedEvent`. + fn set_matrix_event(&self, matrix_event: AnySyncRoomEvent) { + if let AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted( + SyncMessageLikeEvent::Original(_), + )) = matrix_event + { + spawn!(clone!(@weak self as obj => async move { + obj.try_to_decrypt(obj.pure_event().event.cast()).await; + })); } - priv_.pure_event.replace(Some(event)); - - self.notify("event"); + self.imp().matrix_event.replace(Some(matrix_event)); self.notify("activatable"); - self.notify("source"); } - async fn try_to_decrypt(&self, event: Raw) { + /// The deserialized Matrix event of this `SupportedEvent`. + pub fn matrix_event(&self) -> AnySyncRoomEvent { + self.imp().matrix_event.borrow().clone().unwrap() + } + + /// Try to decrypt this `SupportedEvent` with the current room keys. + /// + /// If decryption fails, it will be retried everytime we receive new room + /// keys. + pub async fn try_to_decrypt(&self, event: Raw) { let priv_ = self.imp(); let room = self.room().matrix_room(); let handle = spawn_tokio!(async move { room.decrypt_event(&event).await }); @@ -256,7 +224,10 @@ impl Event { if let Some(keys_handle) = priv_.keys_handle.take() { self.room().disconnect(keys_handle); } - self.set_matrix_pure_event(decrypted.into()); + let pure_event = SyncRoomEvent::from(decrypted); + let matrix_event = pure_event.event.deserialize().unwrap(); + self.set_pure_event(pure_event); + self.set_matrix_event(matrix_event); } Err(error) => { warn!("Failed to decrypt event: {}", error); @@ -264,7 +235,7 @@ impl Event { let handle = self.room().connect_new_encryption_keys( clone!(@weak self as obj => move |_| { // Try to decrypt the event again - obj.set_matrix_pure_event(obj.matrix_pure_event()); + obj.set_matrix_event(obj.matrix_event()); }), ); @@ -274,142 +245,50 @@ impl Event { } } - pub fn matrix_sender(&self) -> OwnedUserId { - let priv_ = self.imp(); - - if let Some(event) = priv_.event.borrow().as_ref() { - event.sender().into() - } else { - priv_ - .pure_event - .borrow() - .as_ref() - .unwrap() - .event - .get_field::("sender") - .unwrap() - .unwrap() - } - } - - pub fn matrix_event_id(&self) -> OwnedEventId { - let priv_ = self.imp(); - - if let Some(event) = priv_.event.borrow().as_ref() { - event.event_id().to_owned() - } else { - priv_ - .pure_event - .borrow() - .as_ref() - .unwrap() - .event - .get_field::("event_id") - .unwrap() - .unwrap() - } - } - - pub fn matrix_transaction_id(&self) -> Option { + /// The event ID of this `SupportedEvent`. + pub fn event_id(&self) -> OwnedEventId { self.imp() - .pure_event + .matrix_event .borrow() .as_ref() .unwrap() - .event - .get_field::("unsigned") - .ok() - .flatten() - .and_then(|unsigned| unsigned.transaction_id) + .event_id() + .to_owned() } - /// The original timestamp of this event. - pub fn matrix_origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { - let priv_ = self.imp(); - if let Some(event) = priv_.event.borrow().as_ref() { - event.origin_server_ts().to_owned() - } else { - priv_ - .pure_event - .borrow() - .as_ref() - .unwrap() - .event - .get_field::("origin_server_ts") - .unwrap() - .unwrap() - } + /// The user ID of the sender of this `SupportedEvent`. + pub fn sender_id(&self) -> OwnedUserId { + self.imp() + .matrix_event + .borrow() + .as_ref() + .unwrap() + .sender() + .to_owned() } - /// The pretty-formatted JSON of this matrix event. - pub fn original_source(&self) -> String { - // We have to convert it to a Value, because a RawValue cannot be - // pretty-printed. - let json: serde_json::Value = serde_json::from_str( - self.imp() - .pure_event - .borrow() - .as_ref() - .unwrap() - .event - .json() - .get(), - ) - .unwrap(); - - serde_json::to_string_pretty(&json).unwrap() + /// The room member that sent this `SupportedEvent`. + pub fn sender(&self) -> Member { + self.room().members().member_by_id(self.sender_id()) } - /// The pretty-formatted JSON used for this matrix event. + /// The transaction ID of this `SupportedEvent`, if any. /// - /// If this matrix event has been replaced, returns the replacing `Event`'s - /// source. - pub fn source(&self) -> String { - self.replacement() - .map(|replacement| replacement.source()) - .unwrap_or_else(|| self.original_source()) - } - - pub fn timestamp(&self) -> DateTime { - let priv_ = self.imp(); - let ts = if let Some(event) = priv_.event.borrow().as_ref() { - event.origin_server_ts().as_secs() - } else { - priv_ - .pure_event - .borrow() - .as_ref() - .unwrap() - .event - .get_field::("origin_server_ts") - .unwrap() - .unwrap() - .as_secs() - }; - - DateTime::from_unix_utc(ts.into()) - .and_then(|t| t.to_local()) + /// This is the random string sent with the event, if it was sent from this + /// session. + pub fn transaction_id(&self) -> Option { + self.imp() + .matrix_event + .borrow() + .as_ref() .unwrap() + .transaction_id() + .map(|txn_id| txn_id.to_owned()) } - pub fn time(&self) -> String { - let datetime = self.timestamp(); - - // FIXME Is there a cleaner way to do that? - let local_time = datetime.format("%X").unwrap().as_str().to_ascii_lowercase(); - - if local_time.ends_with("am") || local_time.ends_with("pm") { - // Use 12h time format (AM/PM) - datetime.format("%l∶%M %p").unwrap().to_string() - } else { - // Use 24 time format - datetime.format("%R").unwrap().to_string() - } - } - - /// Find the related event if any - pub fn related_matrix_event(&self) -> Option { - match self.imp().event.borrow().as_ref()? { + /// The ID of the event this `SupportedEvent` relates to, if any. + pub fn related_event_id(&self) -> Option { + match self.imp().matrix_event.borrow().as_ref()? { AnySyncRoomEvent::MessageLike(ref message) => match message { AnySyncMessageLikeEvent::RoomRedaction(SyncRoomRedactionEvent::Original(event)) => { Some(event.redacts.clone()) @@ -420,8 +299,6 @@ impl Event { AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(event)) => { match &event.content.relates_to { Some(relates_to) => match relates_to { - // TODO: Figure out Relation::Annotation(), Relation::Reference() but - // they are pre-specs for now See: https://github.com/uhoreg/matrix-doc/blob/aggregations-reactions/proposals/2677-reactions.md Relation::Reply { in_reply_to } => Some(in_reply_to.event_id.clone()), Relation::Replacement(replacement) => { Some(replacement.event_id.clone()) @@ -438,63 +315,26 @@ impl Event { } } - /// Whether this event is hidden from the user or displayed in the room - /// history. - pub fn is_hidden_event(&self) -> bool { - let priv_ = self.imp(); - - if self.related_matrix_event().is_some() { - if let Some(AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Original(message), - ))) = priv_.event.borrow().as_ref() - { - if let Some(Relation::Reply { in_reply_to: _ }) = message.content.relates_to { - return false; - } - } - return true; - } - - let event = priv_.event.borrow(); - - // List of all events to be shown. - match event.as_ref() { - Some(AnySyncRoomEvent::MessageLike(message)) => !matches!( - message, - AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(_)) - | AnySyncMessageLikeEvent::RoomEncrypted(SyncMessageLikeEvent::Original(_)) - | AnySyncMessageLikeEvent::Sticker(SyncMessageLikeEvent::Original(_)) - ), - Some(AnySyncRoomEvent::State(state)) => !matches!( - state, - AnySyncStateEvent::RoomCreate(SyncStateEvent::Original(_)) - | AnySyncStateEvent::RoomMember(SyncStateEvent::Original(_)) - | AnySyncStateEvent::RoomThirdPartyInvite(SyncStateEvent::Original(_)) - | AnySyncStateEvent::RoomTombstone(SyncStateEvent::Original(_)) - ), - _ => true, - } - } - - /// Whether this is a replacing `Event`. + /// Whether this `SupportedEvent` replaces another one. /// - /// Replacing matrix events are: + /// Replacing Matrix events are: /// /// - `RoomRedaction` /// - `RoomMessage` with `Relation::Replacement` pub fn is_replacing_event(&self) -> bool { - match self.matrix_event() { - Some(AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + match self.imp().matrix_event.borrow().as_ref().unwrap() { + AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( SyncMessageLikeEvent::Original(message), - ))) => { + )) => { matches!(message.content.relates_to, Some(Relation::Replacement(_))) } - Some(AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(_))) => true, + AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(_)) => true, _ => false, } } - pub fn prepend_replacing_events(&self, events: Vec) { + /// Prepend the given events to the list of replacing events. + pub fn prepend_replacing_events(&self, events: Vec) { let priv_ = self.imp(); priv_.replacing_events.borrow_mut().splice(..0, events); if self.redacted() { @@ -502,7 +342,8 @@ impl Event { } } - pub fn append_replacing_events(&self, events: Vec) { + /// Append the given events to the list of replacing events. + pub fn append_replacing_events(&self, events: Vec) { let priv_ = self.imp(); let old_replacement = self.replacement(); @@ -536,15 +377,14 @@ impl Event { } } - pub fn replacing_events(&self) -> Vec { + /// The replacing events of this `SupportedEvent`, in the order of the + /// timeline. + pub fn replacing_events(&self) -> Vec { self.imp().replacing_events.borrow().clone() } - /// The `Event` that replaces this one, if any. - /// - /// If this matrix event has been redacted or replaced, returns the - /// corresponding `Event`, otherwise returns `None`. - pub fn replacement(&self) -> Option { + /// The event that replaces this `SupportedEvent`, if any. + pub fn replacement(&self) -> Option { self.replacing_events() .iter() .rev() @@ -552,21 +392,19 @@ impl Event { .cloned() } - /// Whether this matrix event has been redacted. + /// Whether this `SupportedEvent` has been redacted. pub fn redacted(&self) -> bool { self.replacement() .filter(|event| { matches!( event.matrix_event(), - Some(AnySyncRoomEvent::MessageLike( - AnySyncMessageLikeEvent::RoomRedaction(_) - )) + AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(_)) ) }) .is_some() } - /// Whether this is a reaction. + /// Whether this `SupportedEvent` is a reaction. pub fn is_reaction(&self) -> bool { matches!( self.original_content(), @@ -574,41 +412,38 @@ impl Event { ) } - /// The reactions for this event. + /// The reactions for this `SupportedEvent`. pub fn reactions(&self) -> &ReactionList { &self.imp().reactions } - /// Add reactions to this event. - pub fn add_reactions(&self, reactions: Vec) { + /// Add reactions to this `SupportedEvent`. + pub fn add_reactions(&self, reactions: Vec) { if !self.redacted() { self.imp().reactions.add_reactions(reactions); } } - /// The content of this matrix event. - /// - /// Returns `None` if this is not a message-like event. + /// The content of this `SupportedEvent`, if this is a message-like event. pub fn original_content(&self) -> Option { - match self.matrix_event()? { + match self.matrix_event() { AnySyncRoomEvent::MessageLike(message) => message.original_content(), _ => None, } } - /// The content to display for this `Event`. - /// - /// If this matrix event has been replaced, returns the replacing `Event`'s - /// content. + /// The content to display for this `SupportedEvent`, if this is a + /// message-like event. /// - /// Returns `None` if this is not a message-like event. + /// If this event has been replaced, returns the replacing + /// `SupportedEvent`'s content. pub fn content(&self) -> Option { self.replacement() .and_then(|replacement| replacement.content()) .or_else(|| self.original_content()) } - /// The content of a media message. + /// Fetch the content of the media message in this `SupportedEvent`. /// /// Compatible events: /// @@ -617,9 +452,11 @@ impl Event { /// - Video message (`MessageType::Video`). /// - Audio message (`MessageType::Audio`). /// - /// Returns `Ok((uid, filename, binary_content))` on success, `Err` if an - /// error occurred while fetching the content. Panics on an incompatible - /// event. `uid` is a unique identifier for this media. + /// Returns `Ok((uid, filename, binary_content))` on success. `uid` is a + /// unique identifier for this media. + /// + /// Returns `Err` if an error occurred while fetching the content. Panics on + /// an incompatible event. pub async fn get_media_content(&self) -> Result<(String, String, Vec), matrix_sdk::Error> { if let AnyMessageLikeEventContent::RoomMessage(content) = self.original_content().unwrap() { let client = self.room().session().client(); @@ -704,7 +541,7 @@ impl Event { panic!("Trying to get the media content of an event of incompatible type"); } - /// Get the id of the event this `Event` replies to, if any. + /// Get the ID of the event this `SupportedEvent` replies to, if any. pub fn reply_to_id(&self) -> Option { match self.original_content()? { AnyMessageLikeEventContent::RoomMessage(message) => { @@ -718,12 +555,12 @@ impl Event { } } - /// Whether this `Event` is a reply to another event. + /// Whether this `SupportedEvent` is a reply to another event. pub fn is_reply(&self) -> bool { self.reply_to_id().is_some() } - /// Get the `Event` this `Event` replies to, if any. + /// Get the `Event` this `SupportedEvent` replies to, if any. /// /// Returns `Ok(None)` if this event is not a reply. pub async fn reply_to_event(&self) -> Result, MatrixError> { @@ -740,4 +577,40 @@ impl Event { .await?; Ok(Some(event)) } + + /// Whether this `SupportedEvent` is hidden from the user or displayed in + /// the room history. + pub fn is_hidden_event(&self) -> bool { + let priv_ = self.imp(); + + if self.related_event_id().is_some() { + if let Some(AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Original(message), + ))) = priv_.matrix_event.borrow().as_ref() + { + if let Some(Relation::Reply { in_reply_to: _ }) = message.content.relates_to { + return false; + } + } + return true; + } + + // List of all events to be shown. + match priv_.matrix_event.borrow().as_ref() { + Some(AnySyncRoomEvent::MessageLike(message)) => !matches!( + message, + AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(_)) + | AnySyncMessageLikeEvent::RoomEncrypted(SyncMessageLikeEvent::Original(_)) + | AnySyncMessageLikeEvent::Sticker(SyncMessageLikeEvent::Original(_)) + ), + Some(AnySyncRoomEvent::State(state)) => !matches!( + state, + AnySyncStateEvent::RoomCreate(SyncStateEvent::Original(_)) + | AnySyncStateEvent::RoomMember(SyncStateEvent::Original(_)) + | AnySyncStateEvent::RoomThirdPartyInvite(SyncStateEvent::Original(_)) + | AnySyncStateEvent::RoomTombstone(SyncStateEvent::Original(_)) + ), + _ => true, + } + } } diff --git a/src/session/room/event/unsupported_event.rs b/src/session/room/event/unsupported_event.rs new file mode 100644 index 00000000..1a6a616e --- /dev/null +++ b/src/session/room/event/unsupported_event.rs @@ -0,0 +1,56 @@ +use gtk::{glib, prelude::*, subclass::prelude::*}; +use matrix_sdk::{deserialized_responses::SyncRoomEvent, ruma::events::RoomEventType}; + +use super::{BoxedSyncRoomEvent, Event, EventImpl}; +use crate::session::room::{ + timeline::{TimelineItem, TimelineItemImpl}, + Room, +}; + +mod imp { + use super::*; + + #[derive(Debug, Default)] + pub struct UnsupportedEvent {} + + #[glib::object_subclass] + impl ObjectSubclass for UnsupportedEvent { + const NAME: &'static str = "RoomUnsupportedEvent"; + type Type = super::UnsupportedEvent; + type ParentType = Event; + } + + impl ObjectImpl for UnsupportedEvent {} + + impl TimelineItemImpl for UnsupportedEvent {} + + impl EventImpl for UnsupportedEvent {} +} + +glib::wrapper! { + /// GObject representation of an unsupported Matrix room event. + pub struct UnsupportedEvent(ObjectSubclass) @extends TimelineItem, Event; +} + +impl UnsupportedEvent { + /// Construct an `UnsupportedEvent` from the given pure event and room. + pub fn new(pure_event: SyncRoomEvent, room: &Room) -> Self { + let pure_event = BoxedSyncRoomEvent(pure_event); + glib::Object::new(&[("pure-event", &pure_event), ("room", room)]) + .expect("Failed to create UnsupportedEvent") + } + + /// The type of this `UnsupportedEvent`, if the field is found. + pub fn event_type(&self) -> Option { + self.upcast_ref::() + .imp() + .pure_event + .borrow() + .as_ref() + .unwrap() + .event + .get_field::("type") + .ok() + .flatten() + } +} diff --git a/src/session/room/event_actions.rs b/src/session/room/event_actions.rs index 5fc36d51..7128486d 100644 --- a/src/session/room/event_actions.rs +++ b/src/session/room/event_actions.rs @@ -5,10 +5,10 @@ use matrix_sdk::ruma::events::{room::message::MessageType, AnyMessageLikeEventCo use once_cell::sync::Lazy; use crate::{ + prelude::*, session::{ event_source_dialog::EventSourceDialog, - room::{Event, RoomAction}, - user::UserExt, + room::{Event, RoomAction, SupportedEvent}, }, spawn, toast, utils::cache_dir, @@ -90,119 +90,122 @@ where }) ); - if let Some(AnyMessageLikeEventContent::RoomMessage(message)) = event.content() { - let user_id = event - .room() - .session() - .user() - .map(|user| user.user_id()) - .unwrap(); - let user = event.room().members().member_by_id(user_id); - if event.sender() == user - || event + if let Some(event) = event.downcast_ref::() { + if let Some(AnyMessageLikeEventContent::RoomMessage(message)) = event.content() { + let user_id = event .room() - .power_levels() - .min_level_for_room_action(&RoomAction::Redact) - <= user.power_level() - { - // Remove message - gtk_macros::action!( - &action_group, - "remove", - clone!(@weak event, => move |_, _| { - event.room().redact(event.matrix_event_id(), None); - }) - ); - } - // Send/redact a reaction - gtk_macros::action!( - &action_group, - "toggle-reaction", - Some(&String::static_variant_type()), - clone!(@weak event => move |_, variant| { - let key: String = variant.unwrap().get().unwrap(); - let room = event.room(); - - let reaction_group = event.reactions().reaction_group_by_key(&key); - - if let Some(reaction) = reaction_group.and_then(|group| group.user_reaction()) { - // The user already sent that reaction, redact it. - room.redact(reaction.matrix_event_id(), None); - } else { - // The user didn't send that redaction, send it. - room.send_reaction(key, event.matrix_event_id()); - } - }) - ); - match message.msgtype { - // Copy Text-Message - MessageType::Text(text_message) => { + .session() + .user() + .map(|user| user.user_id()) + .unwrap(); + let user = event.room().members().member_by_id(user_id); + if event.sender() == user + || event + .room() + .power_levels() + .min_level_for_room_action(&RoomAction::Redact) + <= user.power_level() + { + // Remove message gtk_macros::action!( &action_group, - "copy-text", - clone!(@weak self as widget => move |_, _| { - widget.clipboard().set_text(&text_message.body); + "remove", + clone!(@weak event, => move |_, _| { + event.room().redact(event.event_id(), None); }) ); } - MessageType::File(_) => { - // Save message's file - gtk_macros::action!( - &action_group, - "file-save", - clone!(@weak self as widget, @weak event => move |_, _| { - widget.save_event_file(event); - }) - ); + // Send/redact a reaction + gtk_macros::action!( + &action_group, + "toggle-reaction", + Some(&String::static_variant_type()), + clone!(@weak event => move |_, variant| { + let key: String = variant.unwrap().get().unwrap(); + let room = event.room(); - // Open message's file - gtk_macros::action!( - &action_group, - "file-open", - clone!(@weak self as widget, @weak event => move |_, _| { - widget.open_event_file(event); - }) - ); - } - MessageType::Emote(message) => { - gtk_macros::action!( - &action_group, - "copy-text", - clone!(@weak self as widget, @weak event => move |_, _| { - let display_name = event.sender().display_name(); - let message = display_name + " " + &message.body; - widget.clipboard().set_text(&message); - }) - ); - } - MessageType::Image(_) => { - gtk_macros::action!( - &action_group, - "save-image", - clone!(@weak self as widget, @weak event => move |_, _| { - widget.save_event_file(event); - }) - ); - } - MessageType::Video(_) => { - gtk_macros::action!( - &action_group, - "save-video", - clone!(@weak self as widget, @weak event => move |_, _| { - widget.save_event_file(event); - }) - ); - } - MessageType::Audio(_) => { - gtk_macros::action!( - &action_group, - "save-audio", - clone!(@weak self as widget, @weak event => move |_, _| { + let reaction_group = event.reactions().reaction_group_by_key(&key); + + if let Some(reaction) = reaction_group.and_then(|group| group.user_reaction()) { + // The user already sent that reaction, redact it. + room.redact(reaction.event_id(), None); + } else { + // The user didn't send that redaction, send it. + room.send_reaction(key, event.event_id()); + } + }) + ); + match message.msgtype { + // Copy Text-Message + MessageType::Text(text_message) => { + gtk_macros::action!( + &action_group, + "copy-text", + clone!(@weak self as widget => move |_, _| { + widget.clipboard().set_text(&text_message.body); + }) + ); + } + MessageType::File(_) => { + // Save message's file + gtk_macros::action!( + &action_group, + "file-save", + clone!(@weak self as widget, @weak event => move |_, _| { widget.save_event_file(event); - }) - ); + }) + ); + + // Open message's file + gtk_macros::action!( + &action_group, + "file-open", + clone!(@weak self as widget, @weak event => move |_, _| { + widget.open_event_file(event); + }) + ); + } + MessageType::Emote(message) => { + gtk_macros::action!( + &action_group, + "copy-text", + clone!(@weak self as widget, @weak event => move |_, _| { + let display_name = event.sender().display_name(); + let message = display_name + " " + &message.body; + widget.clipboard().set_text(&message); + }) + ); + } + + MessageType::Image(_) => { + gtk_macros::action!( + &action_group, + "save-image", + clone!(@weak self as widget, @weak event => move |_, _| { + widget.save_event_file(event); + }) + ); + } + MessageType::Video(_) => { + gtk_macros::action!( + &action_group, + "save-video", + clone!(@weak self as widget, @weak event => move |_, _| { + widget.save_event_file(event); + }) + ); + } + MessageType::Audio(_) => { + gtk_macros::action!( + &action_group, + "save-audio", + clone!(@weak self as widget, @weak event => move |_, _| { + widget.save_event_file(event); + }) + ); + } + _ => {} } - _ => {} } } self.insert_action_group("event", Some(&action_group)); @@ -211,9 +214,9 @@ where /// Save the file in `event`. /// - /// See `Event::get_media_content` for compatible events. Panics on an - /// incompatible event. - fn save_event_file(&self, event: Event) { + /// See [`SupportedEvent::get_media_content()`] for compatible events. + /// Panics on an incompatible event. + fn save_event_file(&self, event: SupportedEvent) { let window: Window = self.root().unwrap().downcast().unwrap(); spawn!( glib::PRIORITY_LOW, @@ -260,9 +263,9 @@ where /// Open the file in `event`. /// - /// See `Event::get_media_content` for compatible events. Panics on an - /// incompatible event. - fn open_event_file(&self, event: Event) { + /// See [`SupportedEvent::get_media_content()`] for compatible events. + /// Panics on an incompatible event. + fn open_event_file(&self, event: SupportedEvent) { spawn!( glib::PRIORITY_LOW, clone!(@weak self as obj => async move { diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs index dc9eb0fd..b1faba8c 100644 --- a/src/session/room/mod.rs +++ b/src/session/room/mod.rs @@ -46,7 +46,7 @@ use matrix_sdk::{ use ruma::events::SyncEphemeralRoomEvent; pub use self::{ - event::Event, + event::*, event_actions::EventActions, highlight_flags::HighlightFlags, member::{Member, Membership}, @@ -55,10 +55,7 @@ pub use self::{ reaction_group::ReactionGroup, reaction_list::ReactionList, room_type::RoomType, - timeline::{ - Timeline, TimelineDayDivider, TimelineItem, TimelineItemExt, TimelineNewMessagesDivider, - TimelineSpinner, TimelineState, - }, + timeline::*, }; use super::verification::IdentityVerification; use crate::{ @@ -103,7 +100,7 @@ mod imp { /// The event of the user's read receipt for this room. pub read_receipt: RefCell>, /// The latest read event in the room's timeline. - pub latest_read: RefCell>, + pub latest_read: RefCell>, /// The highlight state of the room, pub highlight: Cell, pub predecessor: OnceCell, @@ -827,7 +824,7 @@ impl Room { if Some(event_id) == self .read_receipt() - .map(|event| event.matrix_event_id()) + .and_then(|event| event.event_id()) .as_deref() { return; @@ -872,7 +869,7 @@ impl Room { timeline .item(i) .as_ref() - .and_then(|obj| obj.downcast_ref::()) + .and_then(|obj| obj.downcast_ref::()) .and_then(|event| { // The user sent the event so it's the latest read event. // Necessary because we don't get read receipts for the user's own events. @@ -886,9 +883,8 @@ impl Room { } // The event is older than the read receipt so it has been read. - if event.matrix_event().filter(count_as_unread).is_some() - && event.matrix_origin_server_ts() - <= read_receipt.matrix_origin_server_ts() + if count_as_unread(&event.matrix_event()) + && event.origin_server_ts() <= read_receipt.origin_server_ts() { return Some(event.to_owned()); } @@ -902,12 +898,12 @@ impl Room { } /// The latest read event in the room's timeline. - pub fn latest_read(&self) -> Option { + pub fn latest_read(&self) -> Option { self.imp().latest_read.borrow().clone() } /// Set the latest read event. - fn set_latest_read(&self, latest_read: Option) { + fn set_latest_read(&self, latest_read: Option) { if latest_read == self.latest_read() { return; } @@ -974,7 +970,7 @@ impl Room { if let Some(event) = timeline .item(i) .as_ref() - .and_then(|obj| obj.downcast_ref::()) + .and_then(|obj| obj.downcast_ref::()) { // This is the event corresponding to the read receipt so there's no unread // messages. @@ -983,7 +979,7 @@ impl Room { } // The user hasn't read the latest message. - if event.matrix_event().filter(count_as_unread).is_some() { + if count_as_unread(&event.matrix_event()) { return false; } } @@ -1306,7 +1302,7 @@ impl Room { }; let raw_event: Raw = Raw::new(&matrix_event).unwrap().cast(); - let event = Event::new(raw_event.into(), self); + let event = SupportedEvent::try_from_event(raw_event.into(), self).unwrap(); self.imp() .timeline .get() @@ -1357,7 +1353,7 @@ impl Room { if let MatrixRoom::Joined(matrix_room) = self.matrix_room() { let raw_event: Raw = Raw::new(&event).unwrap().cast(); - let event = Event::new(raw_event.into(), self); + let event = SupportedEvent::try_from_event(raw_event.into(), self).unwrap(); self.imp() .timeline .get() diff --git a/src/session/room/reaction_group.rs b/src/session/room/reaction_group.rs index 339f7269..6df0dbd4 100644 --- a/src/session/room/reaction_group.rs +++ b/src/session/room/reaction_group.rs @@ -1,7 +1,7 @@ use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*}; -use super::Event; -use crate::session::UserExt; +use super::SupportedEvent; +use crate::prelude::*; mod imp { use std::cell::RefCell; @@ -16,7 +16,7 @@ mod imp { /// The key of the group. pub key: OnceCell, /// The reactions in the group. - pub reactions: RefCell>, + pub reactions: RefCell>, } #[glib::object_subclass] @@ -108,14 +108,14 @@ impl ReactionGroup { } /// The reaction in this group sent by this user, if any. - pub fn user_reaction(&self) -> Option { + pub fn user_reaction(&self) -> Option { let reactions = self.imp().reactions.borrow(); if let Some(user) = reactions .first() .and_then(|event| event.room().session().user().cloned()) { for reaction in reactions.iter().filter(|event| !event.redacted()) { - if reaction.matrix_sender() == user.user_id() { + if reaction.sender_id() == user.user_id() { return Some(reaction.clone()); } } @@ -129,7 +129,7 @@ impl ReactionGroup { } /// Add new reactions to this group. - pub fn add_reactions(&self, new_reactions: Vec) { + pub fn add_reactions(&self, new_reactions: Vec) { let prev_has_user = self.has_user(); let mut added_reactions = Vec::with_capacity(new_reactions.len()); diff --git a/src/session/room/reaction_list.rs b/src/session/room/reaction_list.rs index 8f711464..a3d60072 100644 --- a/src/session/room/reaction_list.rs +++ b/src/session/room/reaction_list.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*}; use matrix_sdk::ruma::events::AnyMessageLikeEventContent; -use super::{Event, ReactionGroup}; +use super::{ReactionGroup, SupportedEvent}; mod imp { use std::cell::RefCell; @@ -45,7 +45,7 @@ mod imp { } glib::wrapper! { - /// List of all `ReactionGroup`s for an `Event`. Implements `ListModel`. + /// List of all `ReactionGroup`s for a `SupportedEvent`. Implements `ListModel`. /// /// `ReactionGroup`s are sorted in "insertion order". pub struct ReactionList(ObjectSubclass) @@ -57,15 +57,15 @@ impl ReactionList { glib::Object::new(&[]).expect("Failed to create ReactionList") } - /// Add reactions with the given reaction `Event`s. + /// Add reactions with the given reaction `SupportedEvent`s. /// - /// Ignores `Event`s that are not reactions. - pub fn add_reactions(&self, new_reactions: Vec) { + /// Ignores `SupportedEvent`s that are not reactions. + pub fn add_reactions(&self, new_reactions: Vec) { let mut reactions = self.imp().reactions.borrow_mut(); let prev_len = reactions.len(); // Group reactions by key - let mut grouped_reactions: HashMap> = HashMap::new(); + let mut grouped_reactions: HashMap> = HashMap::new(); for event in new_reactions { if let Some(AnyMessageLikeEventContent::Reaction(reaction)) = event.content() { let relation = reaction.relates_to; diff --git a/src/session/room/timeline/mod.rs b/src/session/room/timeline/mod.rs index 1e47d51a..32018a1f 100644 --- a/src/session/room/timeline/mod.rs +++ b/src/session/room/timeline/mod.rs @@ -23,10 +23,8 @@ pub use timeline_new_messages_divider::TimelineNewMessagesDivider; pub use timeline_spinner::TimelineSpinner; use tokio::task::JoinHandle; -use crate::{ - session::room::{Event, Room}, - spawn_tokio, -}; +use super::{Event, Room, SupportedEvent, UnsupportedEvent}; +use crate::{prelude::*, spawn_tokio}; #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)] #[repr(u32)] @@ -195,7 +193,7 @@ impl Timeline { let mut previous_timestamp = if position > 0 { list.get(position - 1) .and_then(|item| item.downcast_ref::()) - .map(|event| event.timestamp()) + .and_then(|event| event.timestamp()) } else { None }; @@ -204,7 +202,7 @@ impl Timeline { for current in list.range(position..position + added) { if let Some(current_timestamp) = current .downcast_ref::() - .map(|event| event.timestamp()) + .and_then(|event| event.timestamp()) { if Some(current_timestamp.ymd()) != previous_timestamp.as_ref().map(|t| t.ymd()) { @@ -252,13 +250,13 @@ impl Timeline { let mut previous_sender = if position > 0 { list.get(position - 1) .filter(|item| item.can_hide_header()) - .and_then(|item| item.sender()) + .and_then(|item| item.event_sender()) } else { None }; for current in list.range(position..position + added) { - let current_sender = current.sender(); + let current_sender = current.event_sender(); if !current.can_hide_header() { current.set_show_header(false); @@ -281,7 +279,7 @@ impl Timeline { // Once the sender changes we can be sure that the visibility for headers will // be correct - if next.sender() != previous_sender { + if next.event_sender() != previous_sender { next.set_show_header(true); break; } @@ -300,15 +298,16 @@ impl Timeline { for event in list .range(position as usize..(position + added) as usize) - .filter_map(|item| item.downcast_ref::()) + .filter_map(|item| item.downcast_ref::()) { - if let Some(relates_to) = relates_to_events.remove(&event.matrix_event_id()) { - let mut replacing_events: Vec = vec![]; - let mut reactions: Vec = vec![]; + if let Some(relates_to) = relates_to_events.remove(&event.event_id()) { + let mut replacing_events = vec![]; + let mut reactions = vec![]; for relation_event_id in relates_to { let relation = self .event_by_id(&relation_event_id) + .and_then(|event| event.downcast::().ok()) .expect("Previously known event has disappeared"); if relation.is_replacing_event() { @@ -326,7 +325,7 @@ impl Timeline { event.add_reactions(reactions); if event.redacted() { - redacted_events.insert(event.matrix_event_id()); + redacted_events.insert(event.event_id()); } } } @@ -352,8 +351,8 @@ impl Timeline { let mut list = list.iter(); while let Some(item) = list.next_back() { - if let Some(event) = item.downcast_ref::() { - if redacted_events.remove(&event.matrix_event_id()) { + if let Some(event) = item.downcast_ref::() { + if redacted_events.remove(&event.event_id()) { redacted_events_pos.push(i - 1); } if redacted_events.is_empty() { @@ -412,20 +411,21 @@ impl Timeline { } } - fn add_hidden_events(&self, events: Vec, at_front: bool) { + fn add_hidden_events(&self, events: Vec, at_front: bool) { let priv_ = self.imp(); let mut relates_to_events = priv_.relates_to_events.borrow_mut(); // Group events by related event - let mut new_relations: HashMap> = HashMap::new(); + let mut new_relations: HashMap<_, Vec<_>> = HashMap::new(); for event in events { - if let Some(relates_to) = relates_to_events.remove(&event.matrix_event_id()) { - let mut replacing_events: Vec = vec![]; - let mut reactions: Vec = vec![]; + if let Some(relates_to) = relates_to_events.remove(&event.event_id()) { + let mut replacing_events = vec![]; + let mut reactions = vec![]; for relation_event_id in relates_to { let relation = self .event_by_id(&relation_event_id) + .and_then(|event| event.downcast::().ok()) .expect("Previously known event has disappeared"); if relation.is_replacing_event() { @@ -443,7 +443,7 @@ impl Timeline { event.add_reactions(reactions); } - if let Some(relates_to_event) = event.related_matrix_event() { + if let Some(relates_to_event) = event.related_event_id() { let relations = new_relations.entry(relates_to_event).or_default(); relations.push(event); } @@ -454,53 +454,54 @@ impl Timeline { for (relates_to_event_id, new_relations) in new_relations { if let Some(relates_to_event) = self.event_by_id(&relates_to_event_id) { // Get the relations in relates_to_event otherwise they will be added in - // in items_changed and they might not be added at the right place. - let mut relations: Vec = relates_to_events - .remove(&relates_to_event.matrix_event_id()) + // items_changed and they might not be added at the right place. + let mut relations: Vec<_> = relates_to_events + .remove(&relates_to_event_id) .unwrap_or_default() .into_iter() .map(|event_id| { self.event_by_id(&event_id) + .and_then(|event| event.downcast::().ok()) .expect("Previously known event has disappeared") }) .collect(); - if at_front { - relations.splice(..0, new_relations); - } else { - relations.extend(new_relations); - } + if let Some(relates_to_event) = relates_to_event.downcast_ref::() { + if at_front { + relations.splice(..0, new_relations); + } else { + relations.extend(new_relations); + } - let mut replacing_events: Vec = vec![]; - let mut reactions: Vec = vec![]; + let mut replacing_events = vec![]; + let mut reactions = vec![]; - for relation in relations { - if relation.is_replacing_event() { - replacing_events.push(relation); - } else if relation.is_reaction() { - reactions.push(relation); + for relation in relations { + if relation.is_replacing_event() { + replacing_events.push(relation); + } else if relation.is_reaction() { + reactions.push(relation); + } } - } - if !at_front || relates_to_event.replacing_events().is_empty() { - relates_to_event.append_replacing_events(replacing_events); - } else { - relates_to_event.prepend_replacing_events(replacing_events); - } - relates_to_event.add_reactions(reactions); + if !at_front || relates_to_event.replacing_events().is_empty() { + relates_to_event.append_replacing_events(replacing_events); + } else { + relates_to_event.prepend_replacing_events(replacing_events); + } + relates_to_event.add_reactions(reactions); - if relates_to_event.redacted() { - redacted_events.insert(relates_to_event.matrix_event_id()); + if relates_to_event.redacted() { + redacted_events.insert(relates_to_event.event_id()); + } } } else { // Store the new event if the `related_to` event isn't known, we will update the // `relates_to` once the `related_to` event is added to the list let relates_to_event = relates_to_events.entry(relates_to_event_id).or_default(); - let relations_ids: Vec = new_relations - .iter() - .map(|event| event.matrix_event_id()) - .collect(); + let relations_ids: Vec<_> = + new_relations.iter().map(|event| event.event_id()).collect(); if at_front { relates_to_event.splice(..0, relations_ids); } else { @@ -648,30 +649,43 @@ impl Timeline { }; let mut pending_events = priv_.pending_events.borrow_mut(); - let mut hidden_events: Vec = vec![]; - - for event in batch.into_iter() { - let event_id = event.matrix_event_id(); + let mut hidden_events = vec![]; - if let Some(pending_id) = event - .matrix_transaction_id() - .and_then(|txn_id| pending_events.remove(&txn_id)) + for event in batch { + if let Some(event_id) = event + .downcast_ref::() + .and_then(|event| event.event_id()) { - let mut event_map = priv_.event_map.borrow_mut(); - - if let Some(pending_event) = event_map.remove(&pending_id) { - pending_event.set_matrix_pure_event(event.matrix_pure_event()); - event_map.insert(event_id, pending_event); - }; + priv_.event_map.borrow_mut().insert(event_id, event); added -= 1; - } else { - priv_.event_map.borrow_mut().insert(event_id, event.clone()); - if event.is_hidden_event() { - hidden_events.push(event); + } else if let Ok(event) = event.downcast::() { + let event_id = event.event_id(); + + if let Some(pending_id) = event + .transaction_id() + .and_then(|txn_id| pending_events.remove(&txn_id)) + { + let mut event_map = priv_.event_map.borrow_mut(); + + if let Some(pending_event) = event_map.remove(&pending_id) { + pending_event.set_pure_event(event.pure_event()); + event_map.insert(event_id, pending_event); + }; added -= 1; } else { - priv_.list.borrow_mut().push_back(event.upcast()); + priv_ + .event_map + .borrow_mut() + .insert(event_id, event.clone().upcast()); + if event.is_hidden_event() { + hidden_events.push(event); + added -= 1; + } else { + priv_.list.borrow_mut().push_back(event.upcast()); + } } + } else { + added -= 1; } } @@ -684,18 +698,18 @@ impl Timeline { } /// Append an event that wasn't yet fully sent and received via a sync - pub fn append_pending(&self, txn_id: &TransactionId, event: Event) { + pub fn append_pending(&self, txn_id: &TransactionId, event: SupportedEvent) { let priv_ = self.imp(); priv_ .event_map .borrow_mut() - .insert(event.matrix_event_id(), event.clone()); + .insert(event.event_id(), event.clone().upcast()); priv_ .pending_events .borrow_mut() - .insert(txn_id.to_owned(), event.matrix_event_id()); + .insert(txn_id.to_owned(), event.event_id()); let index = { let mut list = priv_.list.borrow_mut(); @@ -756,22 +770,32 @@ impl Timeline { let mut added = batch.len(); { - let mut hidden_events: Vec = vec![]; + let mut hidden_events: Vec<_> = vec![]; // Extend the size of the list so that rust doesn't need to reallocate memory // multiple times priv_.list.borrow_mut().reserve(added); for event in batch { - priv_ - .event_map - .borrow_mut() - .insert(event.matrix_event_id(), event.clone()); - - if event.is_hidden_event() { - hidden_events.push(event); + if let Some(event_id) = event + .downcast_ref::() + .and_then(|event| event.event_id()) + { + priv_.event_map.borrow_mut().insert(event_id, event); added -= 1; + } else if let Ok(event) = event.downcast::() { + priv_ + .event_map + .borrow_mut() + .insert(event.event_id(), event.clone().upcast()); + + if event.is_hidden_event() { + hidden_events.push(event); + added -= 1; + } else { + priv_.list.borrow_mut().push_front(event.upcast()); + } } else { - priv_.list.borrow_mut().push_front(event.upcast()); + added -= 1; } } self.add_hidden_events(hidden_events, true); diff --git a/src/session/room/timeline/timeline_item.rs b/src/session/room/timeline/timeline_item.rs index 55ef7265..d4cd32b6 100644 --- a/src/session/room/timeline/timeline_item.rs +++ b/src/session/room/timeline/timeline_item.rs @@ -15,7 +15,7 @@ mod imp { pub selectable: fn(&super::TimelineItem) -> bool, pub activatable: fn(&super::TimelineItem) -> bool, pub can_hide_header: fn(&super::TimelineItem) -> bool, - pub sender: fn(&super::TimelineItem) -> Option, + pub event_sender: fn(&super::TimelineItem) -> Option, } unsafe impl ClassStruct for TimelineItemClass { @@ -37,9 +37,9 @@ mod imp { (klass.as_ref().can_hide_header)(this) } - pub(super) fn timeline_item_sender(this: &super::TimelineItem) -> Option { + pub(super) fn timeline_item_event_sender(this: &super::TimelineItem) -> Option { let klass = this.class(); - (klass.as_ref().sender)(this) + (klass.as_ref().event_sender)(this) } #[derive(Debug, Default)] @@ -88,8 +88,8 @@ mod imp { glib::ParamFlags::READABLE, ), glib::ParamSpecObject::new( - "sender", - "Sender", + "event-sender", + "Event Sender", "If this item is a Matrix event, the sender of the event.", Member::static_type(), glib::ParamFlags::READABLE, @@ -119,7 +119,7 @@ mod imp { "activatable" => obj.activatable().to_value(), "show-header" => obj.show_header().to_value(), "can-hide-header" => obj.can_hide_header().to_value(), - "sender" => obj.sender().to_value(), + "event-sender" => obj.event_sender().to_value(), _ => unimplemented!(), } } @@ -163,7 +163,7 @@ pub trait TimelineItemExt: 'static { /// If this is a Matrix event, the sender of the event. /// /// Defaults to `None`. - fn sender(&self) -> Option; + fn event_sender(&self) -> Option; } impl> TimelineItemExt for O { @@ -194,8 +194,8 @@ impl> TimelineItemExt for O { imp::timeline_item_can_hide_header(self.upcast_ref()) } - fn sender(&self) -> Option { - imp::timeline_item_sender(self.upcast_ref()) + fn event_sender(&self) -> Option { + imp::timeline_item_event_sender(self.upcast_ref()) } } @@ -217,7 +217,7 @@ pub trait TimelineItemImpl: ObjectImpl { false } - fn sender(&self, _obj: &Self::Type) -> Option { + fn event_sender(&self, _obj: &Self::Type) -> Option { None } } @@ -236,7 +236,7 @@ where klass.selectable = selectable_trampoline::; klass.activatable = activatable_trampoline::; klass.can_hide_header = can_hide_header_trampoline::; - klass.sender = sender_trampoline::; + klass.event_sender = event_sender_trampoline::; } } @@ -268,11 +268,11 @@ where this.imp().can_hide_header(this) } -fn sender_trampoline(this: &TimelineItem) -> Option +fn event_sender_trampoline(this: &TimelineItem) -> Option where T: ObjectSubclass + TimelineItemImpl, T::Type: IsA, { let this = this.downcast_ref::().unwrap(); - this.imp().sender(this) + this.imp().event_sender(this) }