[fractal/fractal-next] Add wrapper object for room, room events and room members



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]