[fractal/fractal-next] Add wrapper object for room, room events and room members
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] Add wrapper object for room, room events and room members
- Date: Fri, 30 Apr 2021 18:58:09 +0000 (UTC)
commit d98e61687293919c5d781dcff63c08827ba41263
Author: Julian Sparber <julian sparber net>
Date: Tue Apr 27 13:02:33 2021 +0200
Add wrapper object for room, room events and room members
src/main.rs | 1 +
src/meson.build | 12 ++
src/session/categories/categories.rs | 131 ++++++++++++
src/session/categories/category.rs | 133 ++++++++++++
src/session/categories/category_type.rs | 32 +++
src/session/categories/mod.rs | 7 +
src/session/mod.rs | 245 +++++++++++++++++-----
src/session/room/event.rs | 338 ++++++++++++++++++++++++++++++
src/session/room/highlight_flags.rs | 19 ++
src/session/room/item.rs | 236 +++++++++++++++++++++
src/session/room/mod.rs | 12 ++
src/session/room/room.rs | 351 ++++++++++++++++++++++++++++++++
src/session/room/timeline.rs | 350 +++++++++++++++++++++++++++++++
src/session/sidebar/category.rs | 4 +-
src/session/sidebar/room.rs | 2 +-
src/session/user.rs | 171 ++++++++++++++++
src/utils.rs | 53 +++++
17 files changed, 2038 insertions(+), 59 deletions(-)
---
diff --git a/src/main.rs b/src/main.rs
index d29c9b87..bba4ce57 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,6 +9,7 @@ mod config;
mod login;
mod secret;
mod session;
+mod utils;
mod window;
use self::application::Application;
diff --git a/src/meson.build b/src/meson.build
index 9576422b..853bf061 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -25,8 +25,20 @@ sources = files(
'window.rs',
'login.rs',
'secret.rs',
+ 'utils.rs',
+ 'session/user.rs',
'session/mod.rs',
+ 'session/categories/categories.rs',
+ 'session/categories/category.rs',
+ 'session/categories/category_type.rs',
+ 'session/categories/mod.rs',
'session/content.rs',
+ 'session/room/event.rs',
+ 'session/room/highlight_flags.rs',
+ 'session/room/item.rs',
+ 'session/room/mod.rs',
+ 'session/room/room.rs',
+ 'session/room/timeline.rs',
'session/sidebar/mod.rs',
'session/sidebar/category_row.rs',
'session/sidebar/room_row.rs',
diff --git a/src/session/categories/categories.rs b/src/session/categories/categories.rs
new file mode 100644
index 00000000..7802dde9
--- /dev/null
+++ b/src/session/categories/categories.rs
@@ -0,0 +1,131 @@
+use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
+use std::collections::{hash_map::Entry, HashMap};
+
+use crate::session::{
+ categories::{Category, CategoryType},
+ room::Room,
+};
+
+mod imp {
+ use super::*;
+ use std::cell::RefCell;
+
+ #[derive(Debug)]
+ pub struct Categories {
+ pub list: [Category; 5],
+ pub room_map: RefCell<HashMap<Room, CategoryType>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Categories {
+ const NAME: &'static str = "Categories";
+ type Type = super::Categories;
+ type ParentType = glib::Object;
+ type Interfaces = (gio::ListModel,);
+
+ fn new() -> Self {
+ Self {
+ list: [
+ Category::new(CategoryType::Invited),
+ Category::new(CategoryType::Favorite),
+ Category::new(CategoryType::Normal),
+ Category::new(CategoryType::LowPriority),
+ Category::new(CategoryType::Left),
+ ],
+ room_map: Default::default(),
+ }
+ }
+ }
+
+ impl ObjectImpl for Categories {}
+
+ impl ListModelImpl for Categories {
+ fn item_type(&self, _list_model: &Self::Type) -> glib::Type {
+ Category::static_type()
+ }
+ fn n_items(&self, _list_model: &Self::Type) -> u32 {
+ self.list.len() as u32
+ }
+ fn item(&self, _list_model: &Self::Type, position: u32) -> Option<glib::Object> {
+ self.list
+ .get(position as usize)
+ .map(glib::object::Cast::upcast_ref::<glib::Object>)
+ .cloned()
+ }
+ }
+}
+
+glib::wrapper! {
+ pub struct Categories(ObjectSubclass<imp::Categories>)
+ @implements gio::ListModel;
+}
+
+impl Default for Categories {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Categories {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create Categories")
+ }
+
+ pub fn append(&self, rooms: Vec<Room>) {
+ let priv_ = imp::Categories::from_instance(&self);
+
+ for room in rooms {
+ if priv_.room_map.borrow().contains_key(&room) {
+ return;
+ }
+
+ room.connect_notify_local(
+ Some("category"),
+ clone!(@weak self as obj => move |room, _| {
+ obj.move_room(room);
+ }),
+ );
+ // TODO: Add all rooms at once
+ self.move_room(&room);
+ }
+ }
+
+ fn move_room(&self, room: &Room) {
+ let priv_ = imp::Categories::from_instance(&self);
+ let mut room_map = priv_.room_map.borrow_mut();
+
+ let entry = room_map.entry(room.clone());
+
+ match entry {
+ Entry::Occupied(mut entry) => {
+ if entry.get() != &room.category() {
+ entry.insert(room.category());
+ self.remove_from_category(entry.get(), room);
+ self.add_to_category(entry.get(), room);
+ }
+ }
+ Entry::Vacant(entry) => {
+ entry.insert(room.category());
+ self.add_to_category(&room.category(), room);
+ }
+ }
+ }
+
+ fn add_to_category(&self, type_: &CategoryType, room: &Room) {
+ let priv_ = imp::Categories::from_instance(&self);
+
+ let position = priv_.list.iter().position(|item| item.type_() == *type_);
+ if let Some(position) = position {
+ priv_.list.get(position).unwrap().append(room);
+ }
+ }
+
+ fn remove_from_category(&self, type_: &CategoryType, room: &Room) {
+ let priv_ = imp::Categories::from_instance(&self);
+
+ let position = priv_.list.iter().position(|item| item.type_() == *type_);
+ if let Some(position) = position {
+ priv_.list.get(position).unwrap().remove(room);
+ }
+ }
+}
diff --git a/src/session/categories/category.rs b/src/session/categories/category.rs
new file mode 100644
index 00000000..3becb53e
--- /dev/null
+++ b/src/session/categories/category.rs
@@ -0,0 +1,133 @@
+use gtk::{gio, glib, prelude::*, subclass::prelude::*};
+
+use crate::session::{categories::CategoryType, room::Room};
+
+mod imp {
+ use super::*;
+ use std::cell::{Cell, RefCell};
+
+ #[derive(Debug, Default)]
+ pub struct Category {
+ pub list: RefCell<Vec<Room>>,
+ pub type_: Cell<CategoryType>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Category {
+ const NAME: &'static str = "Category";
+ type Type = super::Category;
+ type ParentType = glib::Object;
+ type Interfaces = (gio::ListModel,);
+ }
+
+ impl ObjectImpl for Category {
+ fn properties() -> &'static [glib::ParamSpec] {
+ use once_cell::sync::Lazy;
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_enum(
+ "type",
+ "Type",
+ "The type of this category",
+ CategoryType::static_type(),
+ CategoryType::default() as i32,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ glib::ParamSpec::new_string(
+ "display-name",
+ "Display Name",
+ "The display name of this category",
+ 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() {
+ "type" => {
+ let type_ = value.get().unwrap();
+ self.type_.set(type_);
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "type" => obj.type_().to_value(),
+ "display-name" => obj.type_().to_string().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+
+ impl ListModelImpl for Category {
+ fn item_type(&self, _list_model: &Self::Type) -> glib::Type {
+ Room::static_type()
+ }
+ fn n_items(&self, _list_model: &Self::Type) -> u32 {
+ self.list.borrow().len() as u32
+ }
+ fn item(&self, _list_model: &Self::Type, position: u32) -> Option<glib::Object> {
+ self.list
+ .borrow()
+ .get(position as usize)
+ .map(|o| o.clone().upcast::<glib::Object>())
+ }
+ }
+}
+
+glib::wrapper! {
+ pub struct Category(ObjectSubclass<imp::Category>)
+ @implements gio::ListModel;
+}
+
+impl Category {
+ pub fn new(type_: CategoryType) -> Self {
+ glib::Object::new(&[("type", &type_)]).expect("Failed to create Category")
+ }
+
+ pub fn type_(&self) -> CategoryType {
+ let priv_ = imp::Category::from_instance(self);
+ priv_.type_.get()
+ }
+
+ pub fn append(&self, room: &Room) {
+ let priv_ = imp::Category::from_instance(self);
+ let index = {
+ let mut list = priv_.list.borrow_mut();
+ let index = list.len();
+ list.push(room.clone());
+ index
+ };
+ self.items_changed(index as u32, 0, 1);
+ }
+
+ pub fn remove(&self, room: &Room) {
+ let priv_ = imp::Category::from_instance(self);
+
+ let index = {
+ let mut list = priv_.list.borrow_mut();
+
+ let index = list.iter().position(|item| item == room);
+ if let Some(index) = index {
+ list.remove(index);
+ }
+ index
+ };
+
+ if let Some(index) = index {
+ self.items_changed(index as u32, 1, 0);
+ }
+ }
+}
diff --git a/src/session/categories/category_type.rs b/src/session/categories/category_type.rs
new file mode 100644
index 00000000..8024c555
--- /dev/null
+++ b/src/session/categories/category_type.rs
@@ -0,0 +1,32 @@
+use gettextrs::gettext;
+use gtk::glib;
+
+// TODO: do we also want the categorie `People` and a custom categorie support?
+#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::GEnum)]
+#[repr(u32)]
+#[genum(type_name = "CategoryType")]
+pub enum CategoryType {
+ Invited = 0,
+ Favorite = 1,
+ Normal = 2,
+ LowPriority = 3,
+ Left = 4,
+}
+
+impl Default for CategoryType {
+ fn default() -> Self {
+ CategoryType::Normal
+ }
+}
+
+impl ToString for CategoryType {
+ fn to_string(&self) -> String {
+ match self {
+ CategoryType::Invited => gettext("Invited"),
+ CategoryType::Favorite => gettext("Favorite"),
+ CategoryType::Normal => gettext("Rooms"),
+ CategoryType::LowPriority => gettext("Low Priority"),
+ CategoryType::Left => gettext("Historical"),
+ }
+ }
+}
diff --git a/src/session/categories/mod.rs b/src/session/categories/mod.rs
new file mode 100644
index 00000000..9b644b47
--- /dev/null
+++ b/src/session/categories/mod.rs
@@ -0,0 +1,7 @@
+mod categories;
+mod category;
+mod category_type;
+
+pub use self::categories::Categories;
+pub use self::category::Category;
+pub use self::category_type::CategoryType;
diff --git a/src/session/mod.rs b/src/session/mod.rs
index 00c68303..6938445d 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -1,9 +1,14 @@
+mod categories;
mod content;
+mod room;
mod sidebar;
+mod user;
use self::content::Content;
use self::sidebar::Sidebar;
+use self::user::User;
+use crate::event_from_sync_event;
use crate::secret;
use crate::RUNTIME;
@@ -11,23 +16,32 @@ use adw;
use adw::subclass::prelude::BinImpl;
use gtk::subclass::prelude::*;
use gtk::{self, prelude::*};
-use gtk::{glib, glib::clone, CompositeTemplate};
+use gtk::{glib, glib::clone, glib::SyncSender, CompositeTemplate};
use gtk_macros::send;
-use log::error;
+use log::{error, warn};
use matrix_sdk::api::r0::{
filter::{FilterDefinition, RoomFilter},
session::login,
};
-use matrix_sdk::{self, Client, ClientConfig, RequestConfig, SyncSettings};
+use matrix_sdk::{
+ self,
+ deserialized_responses::SyncResponse,
+ events::{AnyRoomEvent, AnySyncRoomEvent},
+ identifiers::RoomId,
+ Client, ClientConfig, RequestConfig, SyncSettings,
+};
use std::time::Duration;
+use crate::session::categories::Categories;
+
mod imp {
use super::*;
use glib::subclass::{InitializingObject, Signal};
use once_cell::sync::{Lazy, OnceCell};
use std::cell::RefCell;
+ use std::collections::HashMap;
- #[derive(Debug, CompositeTemplate)]
+ #[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/FractalNext/session.ui")]
pub struct Session {
#[template_child]
@@ -38,6 +52,8 @@ mod imp {
/// Contains the error if something went wrong
pub error: RefCell<Option<matrix_sdk::Error>>,
pub client: OnceCell<Client>,
+ pub rooms: RefCell<HashMap<RoomId, room::Room>>,
+ pub categories: Categories,
}
#[glib::object_subclass]
@@ -46,18 +62,23 @@ mod imp {
type Type = super::Session;
type ParentType = adw::Bin;
- fn new() -> Self {
- Self {
- sidebar: TemplateChild::default(),
- content: TemplateChild::default(),
- homeserver: OnceCell::new(),
- error: RefCell::new(None),
- client: OnceCell::new(),
- }
- }
-
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
+ klass.install_action(
+ "session.show-room",
+ Some("s"),
+ move |widget, _, parameter| {
+ use std::convert::TryInto;
+ if let Some(room_id) = parameter
+ .and_then(|p| p.str())
+ .and_then(|s| s.try_into().ok())
+ {
+ widget.handle_show_room_action(room_id);
+ } else {
+ warn!("Not a valid room id: {:?}", parameter);
+ }
+ },
+ );
}
fn instance_init(obj: &InitializingObject<Self>) {
@@ -68,13 +89,22 @@ mod imp {
impl ObjectImpl for Session {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
- vec![glib::ParamSpec::new_string(
- "homeserver",
- "Homeserver",
- "The matrix homeserver of this session",
- None,
- glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
- )]
+ vec![
+ glib::ParamSpec::new_string(
+ "homeserver",
+ "Homeserver",
+ "The matrix homeserver of this session",
+ None,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ glib::ParamSpec::new_object(
+ "categories",
+ "Categories",
+ "A list of rooms grouped into categories",
+ Categories::static_type(),
+ glib::ParamFlags::READABLE,
+ ),
+ ]
});
PROPERTIES.as_ref()
@@ -101,6 +131,7 @@ mod imp {
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"homeserver" => self.homeserver.get().to_value(),
+ "categories" => self.categories.to_value(),
_ => unimplemented!(),
}
}
@@ -150,7 +181,7 @@ impl Session {
}
fn login(&self, method: CreationMethod) {
- let priv_ = &imp::Session::from_instance(self);
+ let priv_ = imp::Session::from_instance(self);
let homeserver = priv_.homeserver.get().unwrap();
let sender = self.setup();
@@ -167,41 +198,50 @@ impl Session {
let client = client.unwrap();
priv_.client.set(client.clone()).unwrap();
-
- RUNTIME.block_on(async {
- tokio::spawn(async move {
- let success = match method {
- CreationMethod::SessionRestore(session) => {
- let res = client.restore_login(session).await;
- let success = res.is_ok();
- send!(sender, res.map(|_| None));
- success
- }
- CreationMethod::Password(username, password) => {
- let response = client
- .login(&username, &password, None, Some("Fractal Next"))
- .await;
- let success = response.is_ok();
- send!(sender, response.map(|r| Some(r)));
- success
- }
- };
-
- if success {
- // We need the filter or else left rooms won't be shown
- let mut room_filter = RoomFilter::empty();
- room_filter.include_leave = true;
-
- let mut filter = FilterDefinition::empty();
- filter.room = room_filter;
-
- let sync_settings = SyncSettings::new()
- .timeout(Duration::from_secs(30))
- .full_state(true)
- .filter(filter.into());
- client.sync(sync_settings).await;
+ let room_sender = self.create_new_sync_response_sender();
+
+ RUNTIME.spawn(async move {
+ let success = match method {
+ CreationMethod::SessionRestore(session) => {
+ let res = client.restore_login(session).await;
+ let success = res.is_ok();
+ send!(sender, res.map(|_| None));
+ success
}
- });
+ CreationMethod::Password(username, password) => {
+ let response = client
+ .login(&username, &password, None, Some("Fractal Next"))
+ .await;
+ let success = response.is_ok();
+ send!(sender, response.map(|r| Some(r)));
+ success
+ }
+ };
+
+ if success {
+ // We need the filter or else left rooms won't be shown
+ let mut room_filter = RoomFilter::empty();
+ room_filter.include_leave = true;
+
+ let mut filter = FilterDefinition::empty();
+ filter.room = room_filter;
+
+ let sync_settings = SyncSettings::new()
+ .timeout(Duration::from_secs(30))
+ .filter(filter.into());
+ client
+ .sync_with_callback(sync_settings, |response| {
+ let room_sender = room_sender.clone();
+ async move {
+ // Using the event hanlder doesn't make a lot of sense for us since we want
every room event
+ // Eventually we should contribute a better EventHandler interface so that it
makes sense to use it.
+ room_sender.send(response).unwrap();
+
+ matrix_sdk::LoopCtrl::Continue
+ }
+ })
+ .await;
+ }
});
}
@@ -239,6 +279,21 @@ impl Session {
sender
}
+ /// Sets up the required channel to receive new room events
+ fn create_new_sync_response_sender(&self) -> SyncSender<SyncResponse> {
+ let (sender, receiver) =
+ glib::MainContext::sync_channel::<SyncResponse>(Default::default(), 100);
+ receiver.attach(
+ None,
+ clone!(@weak self as obj => @default-return glib::Continue(false), move |response| {
+ obj.handle_sync_reposne(response);
+ glib::Continue(true)
+ }),
+ );
+
+ sender
+ }
+
/// Loads the state from the `Store`
/// Note that the `Store` currently doesn't store all events, therefore, we arn't really
/// loading much via this function.
@@ -271,4 +326,82 @@ impl Session {
let homeserver = priv_.homeserver.get().unwrap();
secret::store_session(homeserver, session)
}
+
+ // TODO: handle show room
+ fn handle_show_room_action(&self, room_id: RoomId) {
+ warn!("TODO: implement room action: {:?}", room_id);
+ }
+
+ fn handle_sync_reposne(&self, response: SyncResponse) {
+ let priv_ = imp::Session::from_instance(self);
+
+ let new_rooms_id: Vec<RoomId> = {
+ let rooms_map = priv_.rooms.borrow();
+
+ let new_joined_rooms = response.rooms.leave.iter().filter_map(|(room_id, _)| {
+ if rooms_map.contains_key(room_id) {
+ Some(room_id)
+ } else {
+ None
+ }
+ });
+
+ let new_left_rooms = response.rooms.join.iter().filter_map(|(room_id, _)| {
+ if rooms_map.contains_key(room_id) {
+ Some(room_id)
+ } else {
+ None
+ }
+ });
+ new_joined_rooms.chain(new_left_rooms).cloned().collect()
+ };
+
+ let mut new_rooms = Vec::new();
+ let mut rooms_map = priv_.rooms.borrow_mut();
+
+ for room_id in new_rooms_id {
+ if let Some(matrix_room) = priv_.client.get().unwrap().get_room(&room_id) {
+ let room = room::Room::new(matrix_room);
+ rooms_map.insert(room_id.clone(), room.clone());
+ new_rooms.push(room.clone());
+ }
+ }
+
+ priv_.categories.append(new_rooms);
+
+ for (room_id, matrix_room) in response.rooms.leave {
+ if matrix_room.timeline.events.is_empty() {
+ continue;
+ }
+ if let Some(room) = rooms_map.get(&room_id) {
+ room.append_events(
+ matrix_room
+ .timeline
+ .events
+ .into_iter()
+ .map(|event| event_from_sync_event!(event, room_id))
+ .collect(),
+ );
+ }
+ }
+
+ for (room_id, matrix_room) in response.rooms.join {
+ if matrix_room.timeline.events.is_empty() {
+ continue;
+ }
+
+ if let Some(room) = rooms_map.get(&room_id) {
+ room.append_events(
+ matrix_room
+ .timeline
+ .events
+ .into_iter()
+ .map(|event| event_from_sync_event!(event, room_id))
+ .collect(),
+ );
+ }
+ }
+
+ // TODO: handle StrippedStateEvents for invited rooms
+ }
}
diff --git a/src/session/room/event.rs b/src/session/room/event.rs
new file mode 100644
index 00000000..1da29c8a
--- /dev/null
+++ b/src/session/room/event.rs
@@ -0,0 +1,338 @@
+use chrono::{offset::Local, DateTime};
+use gtk::{glib, prelude::*, subclass::prelude::*};
+use matrix_sdk::{
+ events::{
+ room::message::MessageType, room::message::Relation, AnyMessageEvent,
+ AnyMessageEventContent, AnyRedactedMessageEvent, AnyRedactedStateEvent, AnyRoomEvent,
+ AnyStateEvent,
+ },
+ identifiers::{EventId, UserId},
+};
+
+use crate::fn_event;
+use crate::session::User;
+
+#[derive(Clone, Debug, glib::GBoxed)]
+#[gboxed(type_name = "BoxedAnyRoomEvent")]
+pub struct BoxedAnyRoomEvent(AnyRoomEvent);
+
+mod imp {
+ use super::*;
+ use glib::subclass::Signal;
+ use once_cell::sync::{Lazy, OnceCell};
+ use std::cell::{Cell, RefCell};
+
+ #[derive(Debug, Default)]
+ pub struct Event {
+ pub event: OnceCell<AnyRoomEvent>,
+ pub relates_to: RefCell<Vec<super::Event>>,
+ pub show_header: Cell<bool>,
+ pub sender: OnceCell<User>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Event {
+ const NAME: &'static str = "RoomEvent";
+ type Type = super::Event;
+ type ParentType = glib::Object;
+ }
+
+ impl ObjectImpl for Event {
+ fn signals() -> &'static [Signal] {
+ static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
+ vec![Signal::builder("relates-to-changed", &[], <()>::static_type().into()).build()]
+ });
+ SIGNALS.as_ref()
+ }
+
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_boxed(
+ "event",
+ "event",
+ "The matrix event of this Event",
+ BoxedAnyRoomEvent::static_type(),
+ glib::ParamFlags::WRITABLE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ glib::ParamSpec::new_boolean(
+ "show-header",
+ "Show Header",
+ "Whether this event should show a header or not. This does do nothing if this event
doesn't have a header. ",
+ false,
+ glib::ParamFlags::READWRITE,
+ ),
+ glib::ParamSpec::new_boolean(
+ "can-hide-header",
+ "Can hide header",
+ "Whether this event is allowed to hide it's header or not.",
+ false,
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpec::new_object(
+ "sender",
+ "Sender",
+ "The sender of this matrix event",
+ User::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "event" => {
+ let event = value.get::<BoxedAnyRoomEvent>().unwrap();
+ self.event.set(event.0).unwrap();
+ }
+ "show-header" => {
+ let show_header = value.get().unwrap();
+ let _ = obj.set_show_header(show_header);
+ }
+ "sender" => {
+ let sender = value.get().unwrap();
+ if let Some(sender) = sender {
+ let _ = self.sender.set(sender).unwrap();
+ }
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "sender" => self.sender.get().to_value(),
+ "show-header" => obj.show_header().to_value(),
+ "can-hide-header" => obj.can_hide_header().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+}
+
+glib::wrapper! {
+ pub struct Event(ObjectSubclass<imp::Event>);
+}
+
+// TODO:
+// - [ ] implement operations for events: forward, reply, delete...
+
+/// This is the GObject represatation of a matrix room event
+impl Event {
+ pub fn new(event: &AnyRoomEvent, sender: &User) -> Self {
+ let event = BoxedAnyRoomEvent(event.to_owned());
+ glib::Object::new(&[("event", &event), ("sender", &sender)])
+ .expect("Failed to create Event")
+ }
+
+ pub fn sender(&self) -> &User {
+ let priv_ = imp::Event::from_instance(&self);
+ priv_.sender.get().unwrap()
+ }
+
+ pub fn matrix_event(&self) -> &AnyRoomEvent {
+ let priv_ = imp::Event::from_instance(&self);
+ priv_.event.get().unwrap()
+ }
+
+ pub fn matrix_sender(&self) -> &UserId {
+ let priv_ = imp::Event::from_instance(&self);
+ let event = priv_.event.get().unwrap();
+ fn_event!(event, sender)
+ }
+
+ pub fn matrix_event_id(&self) -> &EventId {
+ let priv_ = imp::Event::from_instance(&self);
+ let event = priv_.event.get().unwrap();
+ fn_event!(event, event_id)
+ }
+
+ pub fn timestamp(&self) -> DateTime<Local> {
+ let priv_ = imp::Event::from_instance(&self);
+ let event = priv_.event.get().unwrap();
+ fn_event!(event, origin_server_ts).clone().into()
+ }
+
+ /// Find the related event if any
+ pub fn related_matrix_event(&self) -> Option<EventId> {
+ match self.matrix_event() {
+ AnyRoomEvent::Message(ref message) => match message {
+ AnyMessageEvent::RoomRedaction(event) => Some(event.redacts.clone()),
+ _ => match message.content() {
+ AnyMessageEventContent::Reaction(event) => Some(event.relation.event_id),
+ AnyMessageEventContent::RoomMessage(event) => match event.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),
+ Relation::Replacement(replacement) => Some(replacement.event_id),
+ _ => None,
+ },
+ _ => None,
+ },
+ // TODO: RoomEncrypted needs https://github.com/ruma/ruma/issues/502
+ _ => None,
+ },
+ },
+ _ => None,
+ }
+ }
+
+ /// Whether this event is hidden from the user or displayed in the room history.
+ pub fn is_hidden_event(&self) -> bool {
+ if self.related_matrix_event().is_some() {
+ return true;
+ }
+
+ match self.matrix_event() {
+ AnyRoomEvent::Message(message) => match message {
+ AnyMessageEvent::CallAnswer(_) => true,
+ AnyMessageEvent::CallInvite(_) => true,
+ AnyMessageEvent::CallHangup(_) => true,
+ AnyMessageEvent::CallCandidates(_) => true,
+ AnyMessageEvent::KeyVerificationReady(_) => true,
+ AnyMessageEvent::KeyVerificationStart(_) => true,
+ AnyMessageEvent::KeyVerificationCancel(_) => true,
+ AnyMessageEvent::KeyVerificationAccept(_) => true,
+ AnyMessageEvent::KeyVerificationKey(_) => true,
+ AnyMessageEvent::KeyVerificationMac(_) => true,
+ AnyMessageEvent::KeyVerificationDone(_) => true,
+ AnyMessageEvent::RoomEncrypted(_) => true,
+ AnyMessageEvent::RoomMessageFeedback(_) => true,
+ AnyMessageEvent::RoomRedaction(_) => true,
+ AnyMessageEvent::Sticker(_) => true,
+ AnyMessageEvent::Custom(_) => true,
+ _ => false,
+ },
+ AnyRoomEvent::State(state) => match state {
+ AnyStateEvent::PolicyRuleRoom(_) => true,
+ AnyStateEvent::PolicyRuleServer(_) => true,
+ AnyStateEvent::PolicyRuleUser(_) => true,
+ AnyStateEvent::RoomAliases(_) => true,
+ AnyStateEvent::RoomAvatar(_) => true,
+ AnyStateEvent::RoomCanonicalAlias(_) => true,
+ AnyStateEvent::RoomEncryption(_) => true,
+ AnyStateEvent::RoomJoinRules(_) => true,
+ AnyStateEvent::RoomName(_) => true,
+ AnyStateEvent::RoomPinnedEvents(_) => true,
+ AnyStateEvent::RoomPowerLevels(_) => true,
+ AnyStateEvent::RoomServerAcl(_) => true,
+ AnyStateEvent::RoomTopic(_) => true,
+ AnyStateEvent::Custom(_) => true,
+ _ => false,
+ },
+ AnyRoomEvent::RedactedMessage(message) => match message {
+ AnyRedactedMessageEvent::CallAnswer(_) => true,
+ AnyRedactedMessageEvent::CallInvite(_) => true,
+ AnyRedactedMessageEvent::CallHangup(_) => true,
+ AnyRedactedMessageEvent::CallCandidates(_) => true,
+ AnyRedactedMessageEvent::KeyVerificationReady(_) => true,
+ AnyRedactedMessageEvent::KeyVerificationStart(_) => true,
+ AnyRedactedMessageEvent::KeyVerificationCancel(_) => true,
+ AnyRedactedMessageEvent::KeyVerificationAccept(_) => true,
+ AnyRedactedMessageEvent::KeyVerificationKey(_) => true,
+ AnyRedactedMessageEvent::KeyVerificationMac(_) => true,
+ AnyRedactedMessageEvent::KeyVerificationDone(_) => true,
+ AnyRedactedMessageEvent::RoomEncrypted(_) => true,
+ AnyRedactedMessageEvent::RoomMessageFeedback(_) => true,
+ AnyRedactedMessageEvent::RoomRedaction(_) => true,
+ AnyRedactedMessageEvent::Sticker(_) => true,
+ AnyRedactedMessageEvent::Custom(_) => true,
+ _ => false,
+ },
+ AnyRoomEvent::RedactedState(state) => match state {
+ AnyRedactedStateEvent::PolicyRuleRoom(_) => true,
+ AnyRedactedStateEvent::PolicyRuleServer(_) => true,
+ AnyRedactedStateEvent::PolicyRuleUser(_) => true,
+ AnyRedactedStateEvent::RoomAliases(_) => true,
+ AnyRedactedStateEvent::RoomAvatar(_) => true,
+ AnyRedactedStateEvent::RoomCanonicalAlias(_) => true,
+ AnyRedactedStateEvent::RoomEncryption(_) => true,
+ AnyRedactedStateEvent::RoomJoinRules(_) => true,
+ AnyRedactedStateEvent::RoomName(_) => true,
+ AnyRedactedStateEvent::RoomPinnedEvents(_) => true,
+ AnyRedactedStateEvent::RoomPowerLevels(_) => true,
+ AnyRedactedStateEvent::RoomServerAcl(_) => true,
+ AnyRedactedStateEvent::RoomTopic(_) => true,
+ AnyRedactedStateEvent::Custom(_) => true,
+ _ => false,
+ },
+ }
+ }
+
+ pub fn set_show_header(&self, visible: bool) {
+ let priv_ = imp::Event::from_instance(&self);
+ if priv_.show_header.get() == visible {
+ return;
+ }
+ priv_.show_header.set(visible);
+ self.notify("show-header");
+ }
+
+ pub fn show_header(&self) -> bool {
+ let priv_ = imp::Event::from_instance(&self);
+
+ priv_.show_header.get()
+ }
+
+ pub fn can_hide_header(&self) -> bool {
+ let priv_ = imp::Event::from_instance(&self);
+ match priv_.event.get().unwrap() {
+ AnyRoomEvent::Message(ref message) => match message.content() {
+ AnyMessageEventContent::RoomMessage(message) => match message.msgtype {
+ MessageType::Audio(_) => true,
+ MessageType::File(_) => true,
+ MessageType::Image(_) => true,
+ MessageType::Location(_) => true,
+ MessageType::Notice(_) => true,
+ MessageType::Text(_) => true,
+ MessageType::Video(_) => true,
+ _ => false,
+ },
+ _ => false,
+ },
+ _ => false,
+ }
+ }
+
+ pub fn add_relates_to(&self, events: Vec<Event>) {
+ let priv_ = imp::Event::from_instance(&self);
+ priv_.relates_to.borrow_mut().extend(events);
+ self.emit_by_name("relates-to-changed", &[]).unwrap();
+ }
+
+ pub fn relates_to(&self) -> Vec<Event> {
+ let priv_ = imp::Event::from_instance(&self);
+ priv_.relates_to.borrow().clone()
+ }
+
+ pub fn connect_relates_to_changed<F: Fn(&Self) + 'static>(
+ &self,
+ f: F,
+ ) -> glib::SignalHandlerId {
+ self.connect_local("relates-to-changed", true, move |values| {
+ let obj = values[0].get::<Self>().unwrap();
+
+ f(&obj);
+
+ None
+ })
+ .unwrap()
+ }
+
+ pub fn connect_show_header_notify<F: Fn(&Self, &glib::ParamSpec) + 'static>(
+ &self,
+ f: F,
+ ) -> glib::SignalHandlerId {
+ self.connect_notify_local(Some("show-header"), f)
+ }
+}
diff --git a/src/session/room/highlight_flags.rs b/src/session/room/highlight_flags.rs
new file mode 100644
index 00000000..cd61adeb
--- /dev/null
+++ b/src/session/room/highlight_flags.rs
@@ -0,0 +1,19 @@
+use gtk::glib;
+
+#[glib::gflags("HighlightFlags")]
+pub enum HighlightFlags {
+ #[glib::gflags(name = "NONE")]
+ NONE = 0b00000000,
+ #[glib::gflags(name = "HIGHLIGHT")]
+ HIGHLIGHT = 0b00000001,
+ #[glib::gflags(name = "BOLD")]
+ BOLD = 0b00000010,
+ #[glib::gflags(skip)]
+ HIGHLIGHT_BOLD = Self::HIGHLIGHT.bits() | Self::BOLD.bits(),
+}
+
+impl Default for HighlightFlags {
+ fn default() -> Self {
+ HighlightFlags::NONE
+ }
+}
diff --git a/src/session/room/item.rs b/src/session/room/item.rs
new file mode 100644
index 00000000..64db4442
--- /dev/null
+++ b/src/session/room/item.rs
@@ -0,0 +1,236 @@
+use chrono::{offset::Local, DateTime};
+use gtk::{glib, prelude::*, subclass::prelude::*};
+use matrix_sdk::{
+ events::AnyRoomEvent,
+ identifiers::{EventId, UserId},
+};
+
+use crate::session::room::Event;
+
+/// This enum contains all possible types the room history can hold.
+#[derive(Debug, Clone)]
+pub enum ItemType {
+ Event(Event),
+ // TODO: Add item type for grouped events
+ DayDivider(DateTime<Local>),
+ NewMessageDivider,
+}
+
+#[derive(Clone, Debug, glib::GBoxed)]
+#[gboxed(type_name = "BoxedItemType")]
+pub struct BoxedItemType(ItemType);
+
+impl From<ItemType> for BoxedItemType {
+ fn from(type_: ItemType) -> Self {
+ BoxedItemType(type_)
+ }
+}
+
+mod imp {
+ use super::*;
+ use once_cell::sync::{Lazy, OnceCell};
+
+ #[derive(Debug, Default)]
+ pub struct Item {
+ pub type_: OnceCell<ItemType>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Item {
+ const NAME: &'static str = "RoomItem";
+ type Type = super::Item;
+ type ParentType = glib::Object;
+ }
+
+ impl ObjectImpl for Item {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_boxed(
+ "type",
+ "Type",
+ "The type of this item",
+ BoxedItemType::static_type(),
+ glib::ParamFlags::WRITABLE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ glib::ParamSpec::new_boolean(
+ "selectable",
+ "Selectable",
+ "Whether this item is selectable or not.",
+ false,
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpec::new_boolean(
+ "show-header",
+ "Show Header",
+ "Whether this item should show a header or not. This does do nothing if this event
doesn't have a header. ",
+ false,
+ glib::ParamFlags::READWRITE,
+ ),
+ glib::ParamSpec::new_boolean(
+ "can-hide-header",
+ "Can hide header",
+ "Whether this item is allowed to hide it's header or not.",
+ false,
+ glib::ParamFlags::READABLE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "type" => {
+ let type_ = value.get::<BoxedItemType>().unwrap();
+ self.type_.set(type_.0).unwrap();
+ }
+ "show-header" => {
+ let show_header = value.get().unwrap();
+ let _ = obj.set_show_header(show_header);
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "selectable" => obj.selectable().to_value(),
+ "show-header" => obj.show_header().to_value(),
+ "can-hide-header" => obj.can_hide_header().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+}
+
+glib::wrapper! {
+ pub struct Item(ObjectSubclass<imp::Item>);
+}
+
+/// This represents any row inside the room history.
+/// This can be AnyRoomEvent, a day divider or new message divider.
+impl Item {
+ pub fn for_event(event: Event) -> Self {
+ let type_ = BoxedItemType(ItemType::Event(event));
+ glib::Object::new(&[("type", &type_)]).expect("Failed to create Item")
+ }
+
+ pub fn for_day_divider(day: DateTime<Local>) -> Self {
+ let type_ = BoxedItemType(ItemType::DayDivider(day));
+ glib::Object::new(&[("type", &type_)]).expect("Failed to create Item")
+ }
+
+ pub fn for_new_message_divider() -> Self {
+ let type_ = BoxedItemType(ItemType::NewMessageDivider);
+ glib::Object::new(&[("type", &type_)]).expect("Failed to create Item")
+ }
+
+ pub fn selectable(&self) -> bool {
+ let priv_ = imp::Item::from_instance(&self);
+ if let ItemType::Event(_event) = priv_.type_.get().unwrap() {
+ true
+ } else {
+ false
+ }
+ }
+
+ pub fn matrix_event(&self) -> Option<&AnyRoomEvent> {
+ let priv_ = imp::Item::from_instance(&self);
+ if let ItemType::Event(event) = priv_.type_.get().unwrap() {
+ Some(event.matrix_event())
+ } else {
+ None
+ }
+ }
+
+ pub fn event(&self) -> Option<&Event> {
+ let priv_ = imp::Item::from_instance(&self);
+ if let ItemType::Event(event) = priv_.type_.get().unwrap() {
+ Some(event)
+ } else {
+ None
+ }
+ }
+
+ pub fn matrix_sender(&self) -> Option<UserId> {
+ let priv_ = imp::Item::from_instance(&self);
+ if let ItemType::Event(event) = priv_.type_.get().unwrap() {
+ Some(event.matrix_sender().clone())
+ } else {
+ None
+ }
+ }
+
+ pub fn matrix_event_id(&self) -> Option<EventId> {
+ let priv_ = imp::Item::from_instance(&self);
+
+ if let ItemType::Event(event) = priv_.type_.get().unwrap() {
+ Some(event.matrix_event_id().clone())
+ } else {
+ None
+ }
+ }
+
+ pub fn event_timestamp(&self) -> Option<DateTime<Local>> {
+ let priv_ = imp::Item::from_instance(&self);
+
+ if let ItemType::Event(event) = priv_.type_.get().unwrap() {
+ Some(event.timestamp())
+ } else {
+ None
+ }
+ }
+
+ pub fn set_show_header(&self, visible: bool) {
+ let priv_ = imp::Item::from_instance(&self);
+ if self.show_header() == visible {
+ return;
+ }
+
+ if let ItemType::Event(event) = priv_.type_.get().unwrap() {
+ event.set_show_header(visible);
+ }
+
+ self.notify("show-header");
+ }
+
+ pub fn show_header(&self) -> bool {
+ let priv_ = imp::Item::from_instance(&self);
+
+ if let ItemType::Event(event) = priv_.type_.get().unwrap() {
+ event.show_header()
+ } else {
+ false
+ }
+ }
+
+ pub fn can_hide_header(&self) -> bool {
+ let priv_ = imp::Item::from_instance(&self);
+
+ if let ItemType::Event(event) = priv_.type_.get().unwrap() {
+ event.can_hide_header()
+ } else {
+ false
+ }
+ }
+
+ pub fn type_(&self) -> &ItemType {
+ let priv_ = imp::Item::from_instance(&self);
+ priv_.type_.get().unwrap()
+ }
+
+ pub fn connect_show_header_notify<F: Fn(&Self, &glib::ParamSpec) + 'static>(
+ &self,
+ f: F,
+ ) -> glib::SignalHandlerId {
+ self.connect_notify_local(Some("show-header"), f)
+ }
+}
diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs
new file mode 100644
index 00000000..1195bfed
--- /dev/null
+++ b/src/session/room/mod.rs
@@ -0,0 +1,12 @@
+mod event;
+mod highlight_flags;
+mod item;
+mod room;
+mod timeline;
+
+pub use self::event::Event;
+pub use self::highlight_flags::HighlightFlags;
+pub use self::item::Item;
+pub use self::item::ItemType;
+pub use self::room::Room;
+pub use self::timeline::Timeline;
diff --git a/src/session/room/room.rs b/src/session/room/room.rs
new file mode 100644
index 00000000..dd9dfb95
--- /dev/null
+++ b/src/session/room/room.rs
@@ -0,0 +1,351 @@
+use gettextrs::gettext;
+use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
+use log::{error, warn};
+use matrix_sdk::{
+ events::{room::member::MemberEventContent, AnyRoomEvent, AnyStateEvent, StateEvent},
+ identifiers::UserId,
+ room::Room as MatrixRoom,
+ RoomMember,
+};
+
+use crate::session::{
+ categories::CategoryType,
+ room::{HighlightFlags, Timeline},
+ User,
+};
+use crate::utils::do_async;
+
+mod imp {
+ use super::*;
+ use once_cell::sync::{Lazy, OnceCell};
+ use std::cell::{Cell, RefCell};
+ use std::collections::HashMap;
+
+ #[derive(Debug, Default)]
+ pub struct Room {
+ pub matrix_room: OnceCell<MatrixRoom>,
+ pub name: RefCell<Option<String>>,
+ pub avatar: RefCell<Option<gio::LoadableIcon>>,
+ pub category: Cell<CategoryType>,
+ pub timeline: OnceCell<Timeline>,
+ pub room_members: RefCell<HashMap<UserId, User>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Room {
+ const NAME: &'static str = "Room";
+ type Type = super::Room;
+ type ParentType = glib::Object;
+ }
+
+ impl ObjectImpl for Room {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_boxed(
+ "matrix-room",
+ "Matrix room",
+ "The underlaying matrix room.",
+ BoxedMatrixRoom::static_type(),
+ glib::ParamFlags::WRITABLE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ glib::ParamSpec::new_string(
+ "display-name",
+ "Display Name",
+ "The display name of this room",
+ None,
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpec::new_object(
+ "avatar",
+ "Avatar",
+ "The url of the avatar of this room",
+ gio::LoadableIcon::static_type(),
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpec::new_object(
+ "timeline",
+ "Timeline",
+ "The timeline of this room",
+ Timeline::static_type(),
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpec::new_flags(
+ "highlight",
+ "Highlight",
+ "How this room is highlighted",
+ HighlightFlags::static_type(),
+ HighlightFlags::default().bits(),
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpec::new_uint64(
+ "notification-count",
+ "Notification count",
+ "The notification count of this room",
+ std::u64::MIN,
+ std::u64::MAX,
+ 0,
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpec::new_enum(
+ "category",
+ "Category",
+ "The category of this room",
+ CategoryType::static_type(),
+ CategoryType::default() as i32,
+ glib::ParamFlags::READABLE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "matrix-room" => {
+ let matrix_room = value.get::<BoxedMatrixRoom>().unwrap();
+ obj.set_matrix_room(matrix_room.0);
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ let matrix_room = self.matrix_room.get().unwrap();
+ match pspec.name() {
+ "display-name" => obj.display_name().to_value(),
+ "avatar" => self.avatar.borrow().to_value(),
+ "timeline" => self.timeline.get().unwrap().to_value(),
+ "category" => obj.category().to_value(),
+ "highlight" => obj.highlight().to_value(),
+ "notification-count" => {
+ let highlight = matrix_room.unread_notification_counts().highlight_count;
+ let notification = matrix_room.unread_notification_counts().notification_count;
+
+ if highlight > 0 {
+ highlight
+ } else {
+ notification
+ }
+ .to_value()
+ }
+ _ => unimplemented!(),
+ }
+ }
+ }
+}
+
+glib::wrapper! {
+ pub struct Room(ObjectSubclass<imp::Room>);
+}
+
+#[derive(Clone, Debug, glib::GBoxed)]
+#[gboxed(type_name = "BoxedMatrixRoom")]
+struct BoxedMatrixRoom(MatrixRoom);
+
+impl Room {
+ pub fn new(room: MatrixRoom) -> Self {
+ glib::Object::new(&[("matrix-room", &BoxedMatrixRoom(room))])
+ .expect("Failed to create Room")
+ }
+
+ pub fn matrix_room(&self) -> &MatrixRoom {
+ let priv_ = imp::Room::from_instance(self);
+ priv_.matrix_room.get().unwrap()
+ }
+
+ fn set_matrix_room(&self, matrix_room: MatrixRoom) {
+ let priv_ = imp::Room::from_instance(self);
+
+ let category = match matrix_room {
+ MatrixRoom::Joined(_) => CategoryType::Normal,
+ MatrixRoom::Invited(_) => CategoryType::Invited,
+ MatrixRoom::Left(_) => CategoryType::Left,
+ };
+
+ priv_.matrix_room.set(matrix_room).unwrap();
+ priv_.timeline.set(Timeline::new(self)).unwrap();
+
+ // We only need to load the room members once, because updates we will receive via state events
+ self.load_members();
+ self.load_display_name();
+ // TODO: change category when room type changes
+ self.set_category(category);
+ }
+
+ pub fn category(&self) -> CategoryType {
+ let priv_ = imp::Room::from_instance(self);
+ priv_.category.get()
+ }
+
+ // TODO: makes this method public and propagate the category to the homeserver via the sdk
+ fn set_category(&self, category: CategoryType) {
+ let priv_ = imp::Room::from_instance(self);
+ if self.category() == category {
+ return;
+ }
+
+ priv_.category.set(category);
+ self.notify("category");
+ }
+
+ pub fn timeline(&self) -> &Timeline {
+ let priv_ = imp::Room::from_instance(self);
+ priv_.timeline.get().unwrap()
+ }
+
+ fn notify_notification_count(&self) {
+ self.notify("highlight");
+ self.notify("notification-count");
+ }
+
+ pub fn highlight(&self) -> HighlightFlags {
+ let priv_ = imp::Room::from_instance(&self);
+ let count = priv_
+ .matrix_room
+ .get()
+ .unwrap()
+ .unread_notification_counts()
+ .highlight_count;
+
+ // TODO: how do we know when to set the row to be bold
+ if count > 0 {
+ HighlightFlags::HIGHLIGHT
+ } else {
+ HighlightFlags::NONE
+ }
+ }
+
+ pub fn display_name(&self) -> String {
+ let priv_ = imp::Room::from_instance(&self);
+ priv_.name.borrow().to_owned().unwrap_or(gettext("Unknown"))
+ }
+
+ fn set_display_name(&self, display_name: Option<String>) {
+ let priv_ = imp::Room::from_instance(&self);
+
+ if Some(self.display_name()) == display_name {
+ return;
+ }
+
+ priv_.name.replace(display_name);
+ self.notify("display-name");
+ }
+
+ fn load_display_name(&self) {
+ let priv_ = imp::Room::from_instance(&self);
+ let matrix_room = priv_.matrix_room.get().unwrap().clone();
+ do_async(
+ async move { matrix_room.display_name().await },
+ clone!(@weak self as obj => move |display_name| async move {
+ // FIXME: We should retry to if the request failed
+ match display_name {
+ Ok(display_name) => obj.set_display_name(Some(display_name)),
+ Err(error) => error!("Couldn't fetch display name: {}", error),
+ };
+ }),
+ );
+ }
+
+ /// Returns the room member `User` object
+ ///
+ /// The returned `User` is specific to this room
+ pub fn member_by_id(&self, user_id: &UserId) -> User {
+ let priv_ = imp::Room::from_instance(self);
+ let mut room_members = priv_.room_members.borrow_mut();
+
+ room_members
+ .entry(user_id.clone())
+ .or_insert(User::new(&user_id))
+ .clone()
+ }
+
+ /// Add new events to the timeline
+ pub fn append_events(&self, batch: Vec<AnyRoomEvent>) {
+ let priv_ = imp::Room::from_instance(self);
+
+ //FIXME: notify only when the count has changed
+ self.notify_notification_count();
+
+ for event in batch.iter() {
+ match event {
+ AnyRoomEvent::State(AnyStateEvent::RoomMember(ref event)) => {
+ self.update_member_for_member_event(event)
+ }
+ AnyRoomEvent::State(AnyStateEvent::RoomName(_)) => {
+ // FIXME: this doesn't take in account changes in the calculated name
+ self.load_display_name()
+ }
+ _ => {}
+ }
+ }
+
+ priv_.timeline.get().unwrap().append(batch);
+ }
+
+ /// Add an initial set of members needed to diplay room events
+ ///
+ /// The `Timeline` makes sure to update the members when a member state event arrives
+ fn add_members(&self, members: Vec<RoomMember>) {
+ let priv_ = imp::Room::from_instance(self);
+ let mut room_members = priv_.room_members.borrow_mut();
+ for member in members {
+ let user_id = member.user_id();
+ let user = room_members
+ .entry(user_id.clone())
+ .or_insert(User::new(user_id));
+ user.update_from_room_member(&member);
+ }
+ }
+
+ /// Updates a room member based on the room member state event
+ fn update_member_for_member_event(&self, event: &StateEvent<MemberEventContent>) {
+ let priv_ = imp::Room::from_instance(self);
+ let mut room_members = priv_.room_members.borrow_mut();
+ let user_id = &event.sender;
+ let user = room_members
+ .entry(user_id.clone())
+ .or_insert(User::new(user_id));
+ user.update_from_member_event(event);
+ }
+
+ fn load_members(&self) {
+ let priv_ = imp::Room::from_instance(self);
+
+ let matrix_room = priv_.matrix_room.get().unwrap().clone();
+ do_async(
+ async move { matrix_room.active_members().await },
+ clone!(@weak self as obj => move |members| async move {
+ // FIXME: We should retry to load the room members if the request failed
+ match members {
+ Ok(members) => obj.add_members(members),
+ Err(error) => error!("Couldn't load room members: {}", error),
+ };
+ }),
+ );
+ }
+
+ pub fn load_previous_events(&self) {
+ warn!("Loading previous evetns is not yet implemented");
+ /*
+ let matrix_room = priv_.matrix_room.get().unwrap().clone();
+ do_async(
+ async move { matrix_room.messages().await },
+ clone!(@weak self as obj => move |events| async move {
+ // FIXME: We should retry to load the room members if the request failed
+ match events {
+ Ok(events) => obj.prepend(events),
+ Err(error) => error!("Couldn't load room members: {}", error),
+ };
+ }),
+ );
+ */
+ }
+}
diff --git a/src/session/room/timeline.rs b/src/session/room/timeline.rs
new file mode 100644
index 00000000..d8b17750
--- /dev/null
+++ b/src/session/room/timeline.rs
@@ -0,0 +1,350 @@
+use gtk::{gio, glib, prelude::*, subclass::prelude::*};
+use matrix_sdk::{events::AnyRoomEvent, identifiers::EventId};
+
+use crate::fn_event;
+use crate::session::room::{Event, Item, Room};
+
+mod imp {
+ use super::*;
+ use once_cell::sync::{Lazy, OnceCell};
+ use std::cell::RefCell;
+ use std::collections::{HashMap, VecDeque};
+
+ #[derive(Debug, Default)]
+ pub struct Timeline {
+ pub room: OnceCell<Room>,
+ pub position_map: RefCell<HashMap<EventId, u32>>,
+ /// A store to keep track of related events that arn't known
+ pub relates_to_events: RefCell<HashMap<EventId, Vec<EventId>>>,
+ /// All events Tilshown in the room history
+ pub list: RefCell<VecDeque<Item>>,
+ /// Events we don't show in the room history
+ pub hidden_events: RefCell<HashMap<EventId, Event>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Timeline {
+ const NAME: &'static str = "Timeline";
+ type Type = super::Timeline;
+ type ParentType = glib::Object;
+ type Interfaces = (gio::ListModel,);
+ }
+
+ impl ObjectImpl for Timeline {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpec::new_object(
+ "room",
+ "Room",
+ "The Room containing this timeline",
+ Room::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "room" => {
+ let room = value.get::<Room>().unwrap();
+ obj.set_room(room);
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "room" => self.room.get().unwrap().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+
+ impl ListModelImpl for Timeline {
+ fn item_type(&self, _list_model: &Self::Type) -> glib::Type {
+ Item::static_type()
+ }
+ fn n_items(&self, _list_model: &Self::Type) -> u32 {
+ self.list.borrow().len() as u32
+ }
+ fn item(&self, _list_model: &Self::Type, position: u32) -> Option<glib::Object> {
+ let list = self.list.borrow();
+
+ list.get(position as usize)
+ .map(|o| o.clone().upcast::<glib::Object>())
+ }
+ }
+}
+
+glib::wrapper! {
+ pub struct Timeline(ObjectSubclass<imp::Timeline>)
+ @implements gio::ListModel;
+}
+
+// TODO:
+// - [ ] Add and handle AnyEphemeralRoomEvent this includes read recipes
+// - [ ] Add new message divider
+impl Timeline {
+ pub fn new(room: &Room) -> Self {
+ glib::Object::new(&[("room", &room)]).expect("Failed to create Timeline")
+ }
+
+ fn items_changed(&self, position: u32, removed: u32, added: u32) {
+ let priv_ = imp::Timeline::from_instance(self);
+
+ // Insert date divider, this needs to happen before updating the position and headers
+ let added = {
+ let position = position as usize;
+ let added = added as usize;
+ let mut list = priv_.list.borrow_mut();
+
+ let mut previous_timestamp = if position > 0 {
+ list.get(position - 1)
+ .and_then(|item| item.event_timestamp())
+ } else {
+ None
+ };
+ let mut divider: Vec<(usize, Item)> = vec![];
+ let mut index = position;
+ for current in list.range(position..position + added) {
+ if let Some(current_timestamp) = current.event_timestamp() {
+ if Some(current_timestamp.date()) != previous_timestamp.map(|t| t.date()) {
+ divider.push((index, Item::for_day_divider(current_timestamp)));
+ previous_timestamp = Some(current_timestamp);
+ }
+ }
+ index += 1;
+ }
+
+ let divider_len = divider.len();
+ for (position, date) in divider {
+ list.insert(position, date);
+ }
+
+ (added + divider_len) as u32
+ };
+
+ // Update the position stored in the `position_map`
+ {
+ let list = priv_.list.borrow();
+ let mut position_map = priv_.position_map.borrow_mut();
+ let mut index = position;
+ for item in list.range((position as usize)..) {
+ if let Some(event_id) = item.matrix_event_id() {
+ position_map.insert(event_id, index);
+ }
+ index += 1;
+ }
+ }
+
+ // Update the header for events that are allowed to hide the header
+ {
+ let position = position as usize;
+ let added = added as usize;
+ let list = priv_.list.borrow();
+
+ let mut previous_sender = if position > 0 {
+ list.get(position - 1)
+ .filter(|event| event.can_hide_header())
+ .and_then(|event| event.matrix_sender())
+ } else {
+ None
+ };
+
+ for current in list.range(position..position + added) {
+ let current_sender = current.matrix_sender();
+
+ if current_sender != previous_sender {
+ current.set_show_header(true);
+ previous_sender = current_sender;
+ } else {
+ current.set_show_header(false);
+ }
+ }
+
+ // Update the events after the new events
+ for next in list.range((position + added)..) {
+ // After an event with non hiddable header the visibility for headers will be correct
+ if !next.can_hide_header() {
+ break;
+ }
+
+ // Once the sender changes we can be sure that the visibility for headers will be correct
+ if next.matrix_sender() != previous_sender {
+ next.set_show_header(true);
+ break;
+ }
+
+ // The `next` has the same sender as the `current`, therefore we don't show the
+ // header and we need to check the event after `next`
+ next.set_show_header(false);
+ }
+ }
+
+ // Update relates_to
+ {
+ let list = priv_.list.borrow();
+ let mut relates_to_events = priv_.relates_to_events.borrow_mut();
+
+ for event in list
+ .range(position as usize..(position + added) as usize)
+ .filter_map(|item| item.event())
+ {
+ if let Some(relates_to_event_id) = event.related_matrix_event() {
+ if let Some(relates_to_event) = self.event_by_id(&relates_to_event_id) {
+ // FIXME: group events and set them all at once, to reduce the emission of notify
+ relates_to_event.add_relates_to(vec![event.to_owned()]);
+ } 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 is added to the list
+ let relates_to_event =
+ relates_to_events.entry(relates_to_event_id).or_default();
+ relates_to_event.push(event.matrix_event_id().to_owned());
+ }
+ }
+
+ if let Some(relates_to) = relates_to_events.remove(event.matrix_event_id()) {
+ event.add_relates_to(
+ relates_to
+ .into_iter()
+ .map(|event_id| {
+ self.event_by_id(&event_id)
+ .expect("Previously known event has disappeared")
+ })
+ .collect(),
+ );
+ }
+ }
+ }
+
+ self.upcast_ref::<gio::ListModel>()
+ .items_changed(position, removed, added);
+ }
+
+ fn add_hidden_event(&self, event: Event) {
+ let priv_ = imp::Timeline::from_instance(self);
+ priv_
+ .hidden_events
+ .borrow_mut()
+ .insert(event.matrix_event_id().to_owned(), event.clone());
+
+ let mut relates_to_events = priv_.relates_to_events.borrow_mut();
+
+ if let Some(relates_to_event_id) = event.related_matrix_event() {
+ if let Some(relates_to_event) = self.event_by_id(&relates_to_event_id) {
+ // FIXME: group events and set them all at once, to reduce the emission of notify
+ relates_to_event.add_relates_to(vec![event.to_owned()]);
+ } 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 is added to the list
+ let relates_to_event = relates_to_events.entry(relates_to_event_id).or_default();
+ relates_to_event.push(event.matrix_event_id().to_owned());
+ }
+ }
+
+ if let Some(relates_to) = relates_to_events.remove(event.matrix_event_id()) {
+ event.add_relates_to(
+ relates_to
+ .into_iter()
+ .map(|event_id| {
+ self.event_by_id(&event_id)
+ .expect("Previously known event has disappeared")
+ })
+ .collect(),
+ );
+ }
+ }
+
+ /// Append the new events
+ // TODO: This should be lazy, for isperation see:
https://blogs.gnome.org/ebassi/documentation/lazy-loading/
+ pub fn append(&self, batch: Vec<AnyRoomEvent>) {
+ let priv_ = imp::Timeline::from_instance(self);
+
+ if batch.is_empty() {
+ return;
+ }
+ let mut added = batch.len();
+
+ let index = {
+ let index = {
+ let mut list = priv_.list.borrow_mut();
+ // Extened the size of the list so that rust doesn't need to realocate memory multiple times
+ list.reserve(batch.len());
+ list.len()
+ };
+
+ for event in batch.into_iter() {
+ let user = self.room().member_by_id(fn_event!(event, sender));
+ let event = Event::new(&event, &user);
+ if event.is_hidden_event() {
+ self.add_hidden_event(event);
+ added -= 1;
+ } else {
+ priv_.list.borrow_mut().push_back(Item::for_event(event));
+ }
+ }
+
+ index
+ };
+
+ self.items_changed(index as u32, 0, added as u32);
+ }
+
+ /// Returns the event with the given id
+ pub fn event_by_id(&self, event_id: &EventId) -> Option<Event> {
+ // TODO: if the referenced event isn't known to us we will need to request it
+ // from the sdk or the matrix homeserver
+ let priv_ = imp::Timeline::from_instance(self);
+ let position_map = priv_.position_map.borrow();
+ let hidden_events_map = priv_.hidden_events.borrow();
+ let list = priv_.list.borrow();
+ position_map
+ .get(event_id)
+ .and_then(|position| list.get(*position as usize))
+ .and_then(|item| item.event().cloned())
+ .or(hidden_events_map.get(event_id).cloned())
+ }
+
+ /// Prepends a batch of events
+ // TODO: This should be lazy, see: https://blogs.gnome.org/ebassi/documentation/lazy-loading/
+ pub fn prepend(&self, batch: Vec<AnyRoomEvent>) {
+ let priv_ = imp::Timeline::from_instance(self);
+ let mut added = batch.len();
+
+ // Extened the size of the list so that rust doesn't need to realocate memory multiple times
+ priv_.list.borrow_mut().reserve(added);
+
+ for event in batch {
+ let user = self.room().member_by_id(fn_event!(event, sender));
+ let event = Event::new(&event, &user);
+
+ if event.is_hidden_event() {
+ self.add_hidden_event(event);
+ added -= 1;
+ } else {
+ priv_.list.borrow_mut().push_front(Item::for_event(event));
+ }
+ }
+
+ self.items_changed(0, 0, added as u32);
+ }
+
+ fn set_room(&self, room: Room) {
+ let priv_ = imp::Timeline::from_instance(self);
+ priv_.room.set(room).unwrap();
+ }
+
+ pub fn room(&self) -> &Room {
+ let priv_ = imp::Timeline::from_instance(self);
+ priv_.room.get().unwrap()
+ }
+}
diff --git a/src/session/sidebar/category.rs b/src/session/sidebar/category.rs
index 75adf489..9e61d541 100644
--- a/src/session/sidebar/category.rs
+++ b/src/session/sidebar/category.rs
@@ -8,7 +8,7 @@ use matrix_sdk::{room::Room as MatrixRoom, RoomType};
// TODO: do we also want the categorie `People` and a custom categorie support?
#[derive(Debug, Eq, PartialEq, Clone, Copy, glib::GEnum)]
#[repr(u32)]
-#[genum(type_name = "CategoryName")]
+#[genum(type_name = "SidebarCategoryName")]
pub enum CategoryName {
Invited = 0,
Favorite = 1,
@@ -66,7 +66,7 @@ mod imp {
#[glib::object_subclass]
impl ObjectSubclass for Category {
- const NAME: &'static str = "Category";
+ const NAME: &'static str = "SidebarCategory";
type Type = super::Category;
type ParentType = glib::Object;
type Interfaces = (gio::ListModel, gtk::SelectionModel);
diff --git a/src/session/sidebar/room.rs b/src/session/sidebar/room.rs
index 47247ad7..446a7b86 100644
--- a/src/session/sidebar/room.rs
+++ b/src/session/sidebar/room.rs
@@ -4,7 +4,7 @@ use gtk::{gio, glib};
use gtk_macros::spawn;
use matrix_sdk::room::Room as MatrixRoom;
-#[glib::gflags("HighlightFlags")]
+#[glib::gflags("SidebarHighlightFlags")]
pub enum HighlightFlags {
#[glib::gflags(name = "NONE")]
NONE = 0b00000000,
diff --git a/src/session/user.rs b/src/session/user.rs
new file mode 100644
index 00000000..32660b47
--- /dev/null
+++ b/src/session/user.rs
@@ -0,0 +1,171 @@
+use gtk::{gio, glib, prelude::*, subclass::prelude::*};
+
+use matrix_sdk::{
+ events::{room::member::MemberEventContent, StateEvent},
+ identifiers::UserId,
+ RoomMember,
+};
+
+mod imp {
+ use super::*;
+ use once_cell::sync::{Lazy, OnceCell};
+ use std::cell::RefCell;
+
+ #[derive(Debug, Default)]
+ pub struct User {
+ pub user_id: OnceCell<String>,
+ pub display_name: RefCell<Option<String>>,
+ pub avatar: RefCell<Option<gio::LoadableIcon>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for User {
+ const NAME: &'static str = "User";
+ type Type = super::User;
+ type ParentType = glib::Object;
+ }
+
+ impl ObjectImpl for User {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_string(
+ "user-id",
+ "User id",
+ "The user id of this user",
+ None,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ glib::ParamSpec::new_string(
+ "display-name",
+ "Display Name",
+ "The display name of the user",
+ None,
+ glib::ParamFlags::READWRITE,
+ ),
+ glib::ParamSpec::new_object(
+ "avatar",
+ "Avatar",
+ "The avatar of this user",
+ gio::LoadableIcon::static_type(),
+ glib::ParamFlags::READABLE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ _obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "user-id" => {
+ let user_id = value.get().unwrap();
+ self.user_id.set(user_id).unwrap();
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "display-name" => obj.display_name().to_value(),
+ "user-id" => self.user_id.get().to_value(),
+ "avatar" => self.avatar.borrow().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+}
+
+glib::wrapper! {
+ pub struct User(ObjectSubclass<imp::User>);
+}
+
+/// This is a `glib::Object` rapresentation of matrix users.
+impl User {
+ pub fn new(user_id: &UserId) -> Self {
+ glib::Object::new(&[("user-id", &user_id.to_string())]).expect("Failed to create User")
+ }
+
+ pub fn display_name(&self) -> String {
+ let priv_ = imp::User::from_instance(&self);
+
+ if let Some(display_name) = priv_.display_name.borrow().to_owned() {
+ display_name
+ } else {
+ priv_
+ .user_id
+ .get()
+ .unwrap()
+ .trim_start_matches("@")
+ .to_owned()
+ }
+ }
+
+ /// Update the user based on the the room member state event
+ //TODO: create the GLoadableIcon and set `avatar`
+ pub fn update_from_room_member(&self, member: &RoomMember) {
+ let changed = {
+ let priv_ = imp::User::from_instance(&self);
+ let user_id = priv_.user_id.get().unwrap();
+ if member.user_id().as_str() != user_id {
+ return;
+ };
+
+ //let content = event.content;
+ let display_name = member.display_name().map(|name| name.to_owned());
+
+ let mut current_display_name = priv_.display_name.borrow_mut();
+ if *current_display_name != display_name {
+ *current_display_name = display_name;
+ true
+ } else {
+ false
+ }
+ };
+
+ if changed {
+ self.notify("display-name");
+ }
+ }
+
+ /// Update the user based on the the room member state event
+ //TODO: create the GLoadableIcon and set `avatar`
+ pub fn update_from_member_event(&self, event: &StateEvent<MemberEventContent>) {
+ let changed = {
+ let priv_ = imp::User::from_instance(&self);
+ let user_id = priv_.user_id.get().unwrap();
+ if event.sender.as_str() != user_id {
+ return;
+ };
+
+ let display_name = if let Some(display_name) = &event.content.displayname {
+ Some(display_name.to_owned())
+ } else {
+ event
+ .content
+ .third_party_invite
+ .as_ref()
+ .map(|i| i.display_name.to_owned())
+ };
+
+ let mut current_display_name = priv_.display_name.borrow_mut();
+ if *current_display_name != display_name {
+ *current_display_name = display_name;
+ true
+ } else {
+ false
+ }
+ };
+
+ if changed {
+ self.notify("display-name");
+ }
+ }
+}
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644
index 00000000..e8930bcb
--- /dev/null
+++ b/src/utils.rs
@@ -0,0 +1,53 @@
+/// FIXME: This should be addressed in ruma direclty
+#[macro_export]
+macro_rules! fn_event {
+ ( $event:ident, $fun:ident ) => {
+ match &$event {
+ AnyRoomEvent::Message(event) => event.$fun(),
+ AnyRoomEvent::State(event) => event.$fun(),
+ AnyRoomEvent::RedactedMessage(event) => event.$fun(),
+ AnyRoomEvent::RedactedState(event) => event.$fun(),
+ }
+ };
+}
+
+/// FIXME: This should be addressed in ruma direclty
+#[macro_export]
+macro_rules! event_from_sync_event {
+ ( $event:ident, $room_id:ident) => {
+ match $event {
+ AnySyncRoomEvent::Message(event) => {
+ AnyRoomEvent::Message(event.into_full_event($room_id.clone()))
+ }
+ AnySyncRoomEvent::State(event) => {
+ AnyRoomEvent::State(event.into_full_event($room_id.clone()))
+ }
+ AnySyncRoomEvent::RedactedMessage(event) => {
+ AnyRoomEvent::RedactedMessage(event.into_full_event($room_id.clone()))
+ }
+ AnySyncRoomEvent::RedactedState(event) => {
+ AnyRoomEvent::RedactedState(event.into_full_event($room_id.clone()))
+ }
+ }
+ };
+}
+
+use crate::RUNTIME;
+use std::future::Future;
+/// Exexcute a future on a tokio runtime and spawn a future on the local thread to handle the result
+pub fn do_async<
+ R: Send + 'static,
+ F1: Future<Output = R> + Send + 'static,
+ F2: Future<Output = ()> + 'static,
+ FN: FnOnce(R) -> F2 + 'static,
+>(
+ tokio_fut: F1,
+ glib_closure: FN,
+) {
+ let (sender, receiver) = futures::channel::oneshot::channel();
+
+ glib::MainContext::default()
+ .spawn_local(async move { glib_closure(receiver.await.unwrap()).await });
+
+ RUNTIME.spawn(async move { sender.send(tokio_fut.await) });
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]