[fractal/fractal-next] sidebar: Add things needed for the Sidebar



commit a22fa19e38c0c65dea44d05b810b851925af07fb
Author: Julian Sparber <julian sparber net>
Date:   Tue Feb 23 16:10:45 2021 +0100

    sidebar: Add things needed for the Sidebar
    
    This includes gobject wrappers and the ui.

 data/resources/resources.gresource.xml     |   4 +
 data/resources/style.css                   |  37 ++++
 data/resources/ui/sidebar-category-item.ui |  52 +++++
 data/resources/ui/sidebar-category-row.ui  |  25 +++
 data/resources/ui/sidebar-room-item.ui     |  30 +++
 data/resources/ui/sidebar-room-row.ui      |  33 ++++
 data/resources/ui/sidebar.ui               |  26 ++-
 src/meson.build                            |   7 +-
 src/session/mod.rs                         |  14 ++
 src/session/sidebar/category.rs            | 299 +++++++++++++++++++++++++++++
 src/session/sidebar/category_list.rs       | 110 +++++++++++
 src/session/sidebar/category_row.rs        | 117 +++++++++++
 src/session/{sidebar.rs => sidebar/mod.rs} |  57 +++++-
 src/session/sidebar/room.rs                | 200 +++++++++++++++++++
 src/session/sidebar/room_row.rs            | 164 ++++++++++++++++
 15 files changed, 1165 insertions(+), 10 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 6d2f72a4..fd8d12a0 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -6,6 +6,10 @@
     <file compressed="true" preprocess="xml-stripblanks" alias="login.ui">ui/login.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="session.ui">ui/session.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="sidebar.ui">ui/sidebar.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-category-item.ui">ui/sidebar-category-item.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-category-row.ui">ui/sidebar-category-row.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-room-item.ui">ui/sidebar-room-item.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="sidebar-room-row.ui">ui/sidebar-room-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="window.ui">ui/window.ui</file>
     <file compressed="true">style.css</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 6f75520d..85ea4858 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -19,3 +19,40 @@
 .send-message-area {
   margin: 6px;
 }
+
+/* Sidebar */
+.sidebar .navigation-sidebar row {  
+  padding: 6px 12px;
+}
+
+.sidebar row .dim-label {  
+  padding: 6px 12px;
+  font-size: 0.8em;
+  font-weight: bold;
+}
+
+.sidebar row .bold {
+  font-weight: bold;
+}
+
+.sidebar .view {
+  padding: 0px;
+}
+
+.sidebar row .notification_count {
+  /* TODO: use correct color variable */
+  background-color: #555;
+  color: white;
+  font-weight: bold;
+  font-size: 0.8em;
+  border-radius: 10px;
+  min-width: 0.7em;
+  padding: 2px 5px;
+}
+
+.sidebar row .highlight {
+  /* TODO: use correct color variable */
+  background-color: @theme_selected_bg_color;
+}
+
+
diff --git a/data/resources/ui/sidebar-category-item.ui b/data/resources/ui/sidebar-category-item.ui
new file mode 100644
index 00000000..21668f30
--- /dev/null
+++ b/data/resources/ui/sidebar-category-item.ui
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="selectable">False</property>
+    <property name="child">
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="focusable">False</property>
+        <child>
+          <object class="FrctlSidebarCategoryRow">
+            <binding name="display-name">
+              <lookup type="FrctlCategory" name="display-name">
+                <lookup name="item">GtkListItem</lookup>
+              </lookup>
+            </binding>
+            <binding name="expanded">
+              <lookup type="FrctlCategory" name="expanded">
+                <lookup name="item">GtkListItem</lookup>
+              </lookup>
+            </binding>
+          </object>
+        </child>
+        <child>
+          <object class="GtkRevealer">
+            <binding name="reveal-child">
+              <lookup type="FrctlCategory" name="expanded">
+                <lookup name="item">GtkListItem</lookup>
+              </lookup>
+            </binding>
+            <property name="child">
+              <object class="GtkListView">
+                <property name="model" bind-source="GtkListItem" bind-property="item" 
bind-flags="sync-create"/>
+                <property name="factory">
+                  <object class="GtkBuilderListItemFactory">
+                    <property name="resource">/org/gnome/FractalNext/sidebar-room-item.ui</property>
+                  </object>
+                </property>
+                <accessibility>
+                  <property name="label" translatable="yes">Room List</property>
+               </accessibility>
+               <style>
+                 <class name="navigation-sidebar"/>
+               </style>
+             </object>
+           </property>
+         </object>
+       </child>
+      </object>
+    </property>
+  </template>
+</interface>
+
diff --git a/data/resources/ui/sidebar-category-row.ui b/data/resources/ui/sidebar-category-row.ui
new file mode 100644
index 00000000..93fdcd91
--- /dev/null
+++ b/data/resources/ui/sidebar-category-row.ui
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FrctlSidebarCategoryRow" parent="AdwBin">
+    <child>
+      <object class="GtkBox">
+       <property name="spacing">6</property>
+       <child>
+         <object class="GtkLabel" id="display_name">
+           <property name="ellipsize">end</property>
+           <style>
+             <class name="dim-label"/>
+           </style>
+         </object>
+       </child>
+       <child type="end">
+         <object class="GtkImage" id="arrow">
+           <style>
+             <class name="arrow"/>
+           </style>
+         </object>
+       </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/resources/ui/sidebar-room-item.ui b/data/resources/ui/sidebar-room-item.ui
new file mode 100644
index 00000000..0a195609
--- /dev/null
+++ b/data/resources/ui/sidebar-room-item.ui
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="FrctlSidebarRoomRow">
+        <binding name="avatar">
+          <lookup type="FrctlRoom" name="avatar">
+            <lookup name="item">GtkListItem</lookup>
+          </lookup>
+        </binding>
+        <binding name="display-name">
+          <lookup type="FrctlRoom" name="display-name">
+            <lookup name="item">GtkListItem</lookup>
+          </lookup>
+        </binding>
+        <binding name="notification-count">
+          <lookup type="FrctlRoom" name="notification-count">
+            <lookup name="item">GtkListItem</lookup>
+          </lookup>
+        </binding>
+        <binding name="highlight">
+          <lookup type="FrctlRoom" name="highlight">
+            <lookup name="item">GtkListItem</lookup>
+          </lookup>
+        </binding>
+      </object>
+    </property>
+  </template>
+</interface>
+
diff --git a/data/resources/ui/sidebar-room-row.ui b/data/resources/ui/sidebar-room-row.ui
new file mode 100644
index 00000000..589578f4
--- /dev/null
+++ b/data/resources/ui/sidebar-room-row.ui
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FrctlSidebarRoomRow" parent="AdwBin">
+    <child>
+      <object class="GtkBox">
+        <property name="spacing">6</property>
+        <child>
+          <object class="AdwAvatar" id="avatar">
+            <property name="show-initials">True</property>
+            <property name="size">24</property>
+            <property name="text" bind-source="display_name" bind-property="label" bind-flags="sync-create" 
/>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="display_name">
+            <property name="ellipsize">end</property>
+          </object>
+        </child>
+        <child type="end">
+          <object class="GtkLabel" id="notification_count">
+            <property name="hexpand">True</property>
+            <property name="halign">end</property>
+            <property name="valign">center</property>
+            <property name="yalign">1.0</property>
+            <style>
+              <class name="notification_count"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/resources/ui/sidebar.ui b/data/resources/ui/sidebar.ui
index 41663796..14b82ace 100644
--- a/data/resources/ui/sidebar.ui
+++ b/data/resources/ui/sidebar.ui
@@ -25,7 +25,7 @@
         <property name="orientation">vertical</property>
         <child>
           <object class="AdwHeaderBar" id="headerbar">
-            <property name="show-end-title-buttons" bind-source="FrctlSidebar" bind-property="compact" 
bind-flags="sync-create" />
+            <property name="show-end-title-buttons" bind-source="FrctlSidebar" bind-property="compact" 
bind-flags="sync-create"/>
             <child type="start">
               <object class="GtkToggleButton" id="search_button">
                 <property name="icon-name">system-search-symbolic</property>
@@ -41,7 +41,7 @@
         </child>
         <child>
           <object class="GtkSearchBar" id="room_search">
-            <property name="search-mode-enabled" bind-source="search_button" bind-property="active" />
+            <property name="search-mode-enabled" bind-source="search_button" bind-property="active"/>
             <property name="child">
               <object class="GtkSearchEntry"/>
             </property>
@@ -49,9 +49,26 @@
         </child>
         <child>
           <object class="GtkScrolledWindow">
-          <property name="vexpand">True</property>
+            <property name="vexpand">True</property>
+            <property name="hscrollbar-policy">never</property>
             <property name="child">
-              <object class="GtkListView" id="listview" />
+             <object class="GtkListView" id="listview">
+               <property name="model">
+                 <object class="GtkNoSelection">
+                   <property name="model">
+                     <object class="FrctlCategoryList" />
+                   </property>
+                 </object>
+               </property>
+               <property name="factory">
+                 <object class="GtkBuilderListItemFactory">
+                   <property name="resource">/org/gnome/FractalNext/sidebar-category-item.ui</property>
+                 </object>
+                </property>
+                <accessibility>
+                  <property name="label" translatable="yes">Sidebar</property>
+                </accessibility>
+              </object>
             </property>
           </object>
         </child>
@@ -59,3 +76,4 @@
     </child>
   </template>
 </interface>
+
diff --git a/src/meson.build b/src/meson.build
index 27d6c064..336f148c 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -27,8 +27,13 @@ sources = files(
   'secret.rs',
   'session/mod.rs',
   'session/content.rs',
-  'session/sidebar.rs',
   'session/supervisor.rs',
+  'session/sidebar/mod.rs',
+  'session/sidebar/category_row.rs',
+  'session/sidebar/room_row.rs',
+  'session/sidebar/category.rs',
+  'session/sidebar/category_list.rs',
+  'session/sidebar/room.rs',
 )
 
 custom_target(
diff --git a/src/session/mod.rs b/src/session/mod.rs
index a131c9e8..76992654 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -39,6 +39,7 @@ mod imp {
         pub homeserver: OnceCell<String>,
         /// Contains the error if something went wrong
         pub error: RefCell<Option<matrix_sdk::Error>>,
+        pub client: OnceCell<Client>,
     }
 
     #[glib::object_subclass]
@@ -53,6 +54,7 @@ mod imp {
                 content: TemplateChild::default(),
                 homeserver: OnceCell::new(),
                 error: RefCell::new(None),
+                client: OnceCell::new(),
             }
         }
 
@@ -171,6 +173,8 @@ impl FrctlSession {
 
         let client = client.unwrap();
 
+        priv_.client.set(client.clone()).unwrap();
+
         let sidebar_sender = priv_.sidebar.get().setup_channel();
         let content_sender = priv_.content.get().setup_channel();
 
@@ -257,6 +261,8 @@ impl FrctlSession {
                     Ok(None) => {}
                 }
 
+                obj.load();
+
                 obj.emit_by_name("ready", &[]).unwrap();
 
                 glib::Continue(false)
@@ -265,6 +271,14 @@ impl FrctlSession {
         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.
+    pub fn load(&self) {
+        let priv_ = imp::FrctlSession::from_instance(self);
+        priv_.sidebar.load(&priv_.client.get().unwrap());
+    }
+
     /// Returns and consumes the `error` that was generated when the session failed to login,
     /// on a successful login this will be `None`.
     /// Unfortunatly it's not possible to connect the Error direclty to the `ready` signals.
diff --git a/src/session/sidebar/category.rs b/src/session/sidebar/category.rs
new file mode 100644
index 00000000..b7f1e098
--- /dev/null
+++ b/src/session/sidebar/category.rs
@@ -0,0 +1,299 @@
+use crate::session::sidebar::FrctlRoom;
+use gettextrs::gettext;
+use gtk::subclass::prelude::*;
+use gtk::{self, gio, glib, prelude::*};
+use matrix_sdk::{identifiers::RoomId, Client};
+use matrix_sdk::{room::Room, 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")]
+pub enum CategoryName {
+    Invited = 0,
+    Favorite = 1,
+    Normal = 2,
+    LowPriority = 3,
+    Left = 4,
+}
+
+impl CategoryName {
+    pub fn get_room_type(&self) -> RoomType {
+        match self {
+            CategoryName::Invited => RoomType::Invited,
+            CategoryName::Favorite => RoomType::Joined,
+            CategoryName::Normal => RoomType::Joined,
+            CategoryName::LowPriority => RoomType::Joined,
+            CategoryName::Left => RoomType::Left,
+        }
+    }
+}
+
+impl Default for CategoryName {
+    fn default() -> Self {
+        CategoryName::Normal
+    }
+}
+
+impl ToString for CategoryName {
+    fn to_string(&self) -> String {
+        match self {
+            CategoryName::Invited => gettext("Invited"),
+            CategoryName::Favorite => gettext("Favorite"),
+            CategoryName::Normal => gettext("Rooms"),
+            CategoryName::LowPriority => gettext("Low Priority"),
+            CategoryName::Left => gettext("Historical"),
+        }
+    }
+}
+
+mod imp {
+    use super::*;
+    use gio::subclass::prelude::*;
+    use once_cell::sync::OnceCell;
+    use std::cell::{Cell, RefCell};
+    use std::collections::HashMap;
+
+    #[derive(Debug)]
+    pub struct FrctlCategory {
+        pub client: OnceCell<Client>,
+        pub map: RefCell<HashMap<RoomId, (u32, FrctlRoom)>>,
+        pub list: RefCell<Vec<RoomId>>,
+        pub name: Cell<CategoryName>,
+        pub expanded: Cell<bool>,
+        pub selected: Cell<u32>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FrctlCategory {
+        const NAME: &'static str = "FrctlCategory";
+        type Type = super::FrctlCategory;
+        type ParentType = glib::Object;
+        type Interfaces = (gio::ListModel, gtk::SelectionModel);
+
+        fn new() -> Self {
+            Self {
+                client: OnceCell::new(),
+                map: RefCell::new(HashMap::new()),
+                list: RefCell::new(Vec::new()),
+                name: Cell::new(CategoryName::default()),
+                expanded: Cell::new(true),
+                selected: Cell::new(u32::MAX),
+            }
+        }
+    }
+
+    impl ObjectImpl for FrctlCategory {
+        fn properties() -> &'static [glib::ParamSpec] {
+            use once_cell::sync::Lazy;
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::enum_(
+                        "display-name",
+                        "Display Name",
+                        "The name of this category",
+                        CategoryName::static_type(),
+                        CategoryName::default() as i32,
+                        glib::ParamFlags::READWRITE,
+                    ),
+                    glib::ParamSpec::boolean(
+                        "expanded",
+                        "Expanded",
+                        "Wheter this category is expanded or not",
+                        true,
+                        glib::ParamFlags::READWRITE,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            _obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.get_name() {
+                "expanded" => {
+                    let expanded: Option<bool> = value
+                        .get()
+                        .expect("type conformity checked by `Object::set_property`");
+                    self.expanded.set(expanded.unwrap());
+                }
+                "display-name" => {
+                    let name = value
+                        .get()
+                        .expect("type conformity checked by `Object::set_property`");
+                    self.name.set(name.unwrap());
+                }
+                _ => unimplemented!(),
+            }
+        }
+
+        fn get_property(
+            &self,
+            _obj: &Self::Type,
+            _id: usize,
+            pspec: &glib::ParamSpec,
+        ) -> glib::Value {
+            match pspec.get_name() {
+                "display-name" => self.name.get().to_value(),
+                "expanded" => self.expanded.get().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    impl ListModelImpl for FrctlCategory {
+        fn get_item_type(&self, _list_model: &Self::Type) -> glib::Type {
+            FrctlRoom::static_type()
+        }
+        fn get_n_items(&self, _list_model: &Self::Type) -> u32 {
+            self.list.borrow().len() as u32
+        }
+        fn get_item(&self, _list_model: &Self::Type, position: u32) -> Option<glib::Object> {
+            let list = self.list.borrow();
+            let room_id = list.get(position as usize);
+            if let Some(room_id) = room_id {
+                self.map
+                    .borrow()
+                    .get(&room_id)
+                    .map(|(_, o)| o.clone().upcast::<glib::Object>())
+            } else {
+                None
+            }
+        }
+    }
+    impl SelectionModelImpl for FrctlCategory {
+        fn get_selection_in_range(
+            &self,
+            _model: &Self::Type,
+            _position: u32,
+            _n_items: u32,
+        ) -> gtk::Bitset {
+            let result = gtk::Bitset::new_empty();
+            if self.selected.get() != u32::MAX {
+                result.add(self.selected.get());
+            }
+            result
+        }
+
+        fn is_selected(&self, _model: &Self::Type, position: u32) -> bool {
+            self.selected.get() == position
+        }
+
+        fn select_item(&self, model: &Self::Type, position: u32, _unselect_rest: bool) -> bool {
+            model.select(position);
+            true
+        }
+    }
+}
+
+glib::wrapper! {
+    pub struct FrctlCategory(ObjectSubclass<imp::FrctlCategory>)
+        @implements gio::ListModel, gtk::SelectionModel;
+}
+
+// TODO: sort the rooms in FrctlCategory, i guess we want last active room first
+impl FrctlCategory {
+    pub fn new(client: Client, name: CategoryName) -> Self {
+        let obj =
+            glib::Object::new(&[("display-name", &name)]).expect("Failed to create FrctlCategory");
+        // We don't need to set the client as a GObject property since it's used only internally
+        let priv_ = imp::FrctlCategory::from_instance(&obj);
+        priv_.client.set(client).unwrap();
+        obj
+    }
+
+    pub fn select(&self, position: u32) {
+        let priv_ = imp::FrctlCategory::from_instance(self);
+        let old_position = priv_.selected.get();
+
+        if position == old_position {
+            return;
+        }
+
+        priv_.selected.set(position);
+
+        if old_position == u32::MAX {
+            self.selection_changed(position, 1);
+        } else if position == u32::MAX {
+            self.selection_changed(old_position, 1);
+        } else if position < old_position {
+            self.selection_changed(position, old_position - position + 1);
+        } else {
+            self.selection_changed(old_position, position - old_position + 1);
+        }
+    }
+
+    pub fn unselect(&self) {
+        self.select(u32::MAX);
+    }
+
+    pub fn update(&self, room_id: &RoomId) {
+        let priv_ = imp::FrctlCategory::from_instance(self);
+        let category_type = priv_.name.get().get_room_type();
+        let client = priv_.client.get().unwrap();
+        let room: Option<Room> = match category_type {
+            RoomType::Invited => client.get_invited_room(room_id).map(Into::into),
+            RoomType::Joined => client.get_joined_room(room_id).map(Into::into),
+            RoomType::Left => client.get_left_room(room_id).map(Into::into),
+        };
+
+        let mut found = false;
+        if let Some((_, room_obj)) = priv_.map.borrow().get(room_id) {
+            if room.is_some() {
+                room_obj.update();
+                found = true;
+            }
+        }
+
+        if found && room.is_none() {
+            if let Some((position, _)) = priv_.map.borrow_mut().remove(&room_id.clone()) {
+                priv_.list.borrow_mut().remove(position as usize);
+                self.items_changed(position, 1, 0);
+            }
+        } else if !found {
+            if let Some(room) = room {
+                self.append(&room);
+            }
+        }
+    }
+
+    pub fn append(&self, room: &Room) {
+        let priv_ = imp::FrctlCategory::from_instance(self);
+        let room_id = room.room_id();
+        let room_obj = FrctlRoom::new(room);
+        let index = {
+            let mut map = priv_.map.borrow_mut();
+            let mut list = priv_.list.borrow_mut();
+            let index = list.len();
+            map.insert(room_id.clone(), (index as u32, room_obj));
+            list.push(room_id.clone());
+            index
+        };
+        self.items_changed(index as u32, 0, 1);
+    }
+
+    pub fn append_batch(&self, rooms: Vec<Room>) {
+        let priv_ = imp::FrctlCategory::from_instance(self);
+        let index = {
+            let mut map = priv_.map.borrow_mut();
+            let mut list = priv_.list.borrow_mut();
+            let index = list.len();
+            let mut position = index;
+            for room in &rooms {
+                let room_id = room.room_id();
+                let room_obj = FrctlRoom::new(room);
+                map.insert(room_id.clone(), (position as u32, room_obj));
+                list.push(room_id.clone());
+                position += 1;
+            }
+            index
+        };
+        self.items_changed(index as u32, 0, rooms.len() as u32);
+    }
+}
diff --git a/src/session/sidebar/category_list.rs b/src/session/sidebar/category_list.rs
new file mode 100644
index 00000000..95c06af0
--- /dev/null
+++ b/src/session/sidebar/category_list.rs
@@ -0,0 +1,110 @@
+use crate::session::sidebar::FrctlCategory;
+use gtk::subclass::prelude::*;
+use gtk::{self, gio, glib, glib::clone, prelude::*};
+use matrix_sdk::identifiers::RoomId;
+
+mod imp {
+    use super::*;
+    use gio::subclass::prelude::*;
+    use std::cell::RefCell;
+
+    #[derive(Debug, Default)]
+    pub struct FrctlCategoryList {
+        pub list: RefCell<Vec<FrctlCategory>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FrctlCategoryList {
+        const NAME: &'static str = "FrctlCategoryList";
+        type Type = super::FrctlCategoryList;
+        type ParentType = glib::Object;
+        type Interfaces = (gio::ListModel,);
+    }
+
+    impl ObjectImpl for FrctlCategoryList {}
+
+    impl ListModelImpl for FrctlCategoryList {
+        fn get_item_type(&self, _list_model: &Self::Type) -> glib::Type {
+            FrctlCategory::static_type()
+        }
+        fn get_n_items(&self, _list_model: &Self::Type) -> u32 {
+            self.list.borrow().len() as u32
+        }
+        fn get_item(&self, _list_model: &Self::Type, position: u32) -> Option<glib::Object> {
+            self.list
+                .borrow()
+                .get(position as usize)
+                .map(glib::object::Cast::upcast_ref::<glib::Object>)
+                .cloned()
+        }
+    }
+}
+
+glib::wrapper! {
+    pub struct FrctlCategoryList(ObjectSubclass<imp::FrctlCategoryList>)
+        @implements gio::ListModel;
+}
+// TODO allow moving between categories
+// TODO allow selection only in one category
+
+impl FrctlCategoryList {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create FrctlCategoryList")
+    }
+
+    pub fn update(&self, room_id: &RoomId) {
+        let priv_ = imp::FrctlCategoryList::from_instance(self);
+        let list = priv_.list.borrow();
+        for category in list.iter() {
+            category.update(room_id);
+        }
+    }
+
+    pub fn append(&self, category: FrctlCategory) {
+        let priv_ = imp::FrctlCategoryList::from_instance(self);
+        let index = {
+            let mut list = priv_.list.borrow_mut();
+            category.connect_selection_changed(
+                clone!(@weak self as obj => move |category, position, _| {
+                    if category.is_selected(position) {
+                        obj.unselect_other_lists(&category);
+                    }
+                }),
+            );
+            list.push(category);
+            list.len() - 1
+        };
+        self.items_changed(index as u32, 0, 1);
+    }
+
+    fn unselect_other_lists(&self, category: &FrctlCategory) {
+        let priv_ = imp::FrctlCategoryList::from_instance(self);
+        let list = priv_.list.borrow();
+
+        for item in list.iter() {
+            if item != category {
+                item.unselect();
+            }
+        }
+    }
+
+    pub fn append_batch(&self, batch: &[FrctlCategory]) {
+        let priv_ = imp::FrctlCategoryList::from_instance(self);
+        let index = {
+            let mut list = priv_.list.borrow_mut();
+            let index = list.len();
+            for category in batch.iter() {
+                category.connect_selection_changed(
+                    clone!(@weak self as obj => move |category, position, _| {
+                        if category.is_selected(position) {
+                            obj.unselect_other_lists(&category);
+                        }
+                    }),
+                );
+                list.push(category.clone());
+            }
+            index
+        };
+        self.items_changed(index as u32, 0, batch.len() as u32);
+    }
+}
diff --git a/src/session/sidebar/category_row.rs b/src/session/sidebar/category_row.rs
new file mode 100644
index 00000000..936d6e5d
--- /dev/null
+++ b/src/session/sidebar/category_row.rs
@@ -0,0 +1,117 @@
+use crate::session::sidebar::CategoryName;
+use adw;
+use adw::subclass::prelude::BinImpl;
+use gtk::subclass::prelude::*;
+use gtk::{self, prelude::*};
+use gtk::{glib, CompositeTemplate};
+
+mod imp {
+    use super::*;
+    use glib::subclass::InitializingObject;
+
+    #[derive(Debug, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/sidebar-category-row.ui")]
+    pub struct FrctlSidebarCategoryRow {
+        #[template_child]
+        pub display_name: TemplateChild<gtk::Label>,
+        #[template_child]
+        pub arrow: TemplateChild<gtk::Image>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FrctlSidebarCategoryRow {
+        const NAME: &'static str = "FrctlSidebarCategoryRow";
+        type Type = super::FrctlSidebarCategoryRow;
+        type ParentType = adw::Bin;
+
+        fn new() -> Self {
+            Self {
+                display_name: TemplateChild::default(),
+                arrow: TemplateChild::default(),
+            }
+        }
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for FrctlSidebarCategoryRow {
+        fn properties() -> &'static [glib::ParamSpec] {
+            use once_cell::sync::Lazy;
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::enum_(
+                        "display-name",
+                        "Display Name",
+                        "The display name of this category",
+                        CategoryName::static_type(),
+                        CategoryName::default() as i32,
+                        glib::ParamFlags::WRITABLE,
+                    ),
+                    glib::ParamSpec::boolean(
+                        "expanded",
+                        "Expanded",
+                        "Wheter this category is expanded or not",
+                        true,
+                        glib::ParamFlags::WRITABLE,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            _obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.get_name() {
+                "display-name" => {
+                    let display_name: CategoryName = value
+                        .get()
+                        .expect("type conformity checked by `Object::set_property`")
+                        .expect("A room always needs a display name");
+                    self.display_name.set_label(&display_name.to_string());
+                }
+                "expanded" => {
+                    let expanded = value
+                        .get()
+                        .expect("type conformity checked by `Object::set_property`")
+                        .unwrap();
+                    if expanded {
+                        //self.add_css_class("expanded");
+                    } else {
+                        //self.remove_css_class("expanded");
+                    }
+                }
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+        }
+    }
+
+    impl WidgetImpl for FrctlSidebarCategoryRow {}
+    impl BinImpl for FrctlSidebarCategoryRow {}
+}
+
+glib::wrapper! {
+    pub struct FrctlSidebarCategoryRow(ObjectSubclass<imp::FrctlSidebarCategoryRow>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl FrctlSidebarCategoryRow {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create FrctlSidebarCategoryRow")
+    }
+}
diff --git a/src/session/sidebar.rs b/src/session/sidebar/mod.rs
similarity index 63%
rename from src/session/sidebar.rs
rename to src/session/sidebar/mod.rs
index 19396b39..5f892cb2 100644
--- a/src/session/sidebar.rs
+++ b/src/session/sidebar/mod.rs
@@ -1,8 +1,20 @@
+mod category;
+mod category_list;
+mod category_row;
+mod room;
+mod room_row;
+
+use self::category::{CategoryName, FrctlCategory};
+use self::category_list::FrctlCategoryList;
+use self::category_row::FrctlSidebarCategoryRow;
+use self::room::{FrctlRoom, HighlightFlags};
+use self::room_row::FrctlSidebarRoomRow;
+
 use adw;
 use adw::subclass::prelude::BinImpl;
 use gtk::subclass::prelude::*;
 use gtk::{self, prelude::*};
-use gtk::{glib, glib::SyncSender, CompositeTemplate};
+use gtk::{glib, glib::clone, glib::SyncSender, CompositeTemplate};
 use matrix_sdk::{identifiers::RoomId, Client};
 
 mod imp {
@@ -35,6 +47,9 @@ mod imp {
         }
 
         fn class_init(klass: &mut Self::Class) {
+            FrctlCategoryList::static_type();
+            FrctlSidebarRoomRow::static_type();
+            FrctlSidebarCategoryRow::static_type();
             Self::bind_template(klass);
         }
 
@@ -107,10 +122,42 @@ impl FrctlSidebar {
     /// Sets up the required channel to recive async updates from the `Client`
     pub fn setup_channel(&self) -> SyncSender<RoomId> {
         let (sender, receiver) = glib::MainContext::sync_channel::<RoomId>(Default::default(), 100);
-        receiver.attach(None, move |_room_id| {
-            //TODO: actually do something: update the message GListModel
-            glib::Continue(true)
-        });
+
+        receiver.attach(
+            None,
+            clone!(@weak self as obj => move |room_id| {
+                obj.get_list_model().update(&room_id);
+                glib::Continue(true)
+            }),
+        );
         sender
     }
+
+    /// Loads the state from the `Store`
+    pub fn load(&self, client: &Client) {
+        let list = self.get_list_model();
+        // TODO: Add list for user defined categories e.g. favorite
+        let invited = FrctlCategory::new(client.clone(), CategoryName::Invited);
+        let joined = FrctlCategory::new(client.clone(), CategoryName::Normal);
+        let left = FrctlCategory::new(client.clone(), CategoryName::Left);
+
+        invited.append_batch(client.invited_rooms().into_iter().map(Into::into).collect());
+        joined.append_batch(client.joined_rooms().into_iter().map(Into::into).collect());
+        left.append_batch(client.left_rooms().into_iter().map(Into::into).collect());
+
+        list.append_batch(&[invited, joined, left]);
+    }
+
+    fn get_list_model(&self) -> FrctlCategoryList {
+        imp::FrctlSidebar::from_instance(self)
+            .listview
+            .get_model()
+            .unwrap()
+            .downcast::<gtk::NoSelection>()
+            .unwrap()
+            .get_model()
+            .unwrap()
+            .downcast::<FrctlCategoryList>()
+            .unwrap()
+    }
 }
diff --git a/src/session/sidebar/room.rs b/src/session/sidebar/room.rs
new file mode 100644
index 00000000..5b53cd6d
--- /dev/null
+++ b/src/session/sidebar/room.rs
@@ -0,0 +1,200 @@
+use gtk::subclass::prelude::*;
+use gtk::{self, prelude::*};
+use gtk::{gio, glib};
+use gtk_macros::spawn;
+use matrix_sdk::room::Room;
+
+#[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
+    }
+}
+
+mod imp {
+    use super::*;
+    use once_cell::sync::OnceCell;
+    use std::cell::RefCell;
+
+    #[derive(Debug)]
+    pub struct FrctlRoom {
+        pub room: OnceCell<Room>,
+        pub name: RefCell<Option<String>>,
+        pub avatar: RefCell<Option<gio::LoadableIcon>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FrctlRoom {
+        const NAME: &'static str = "FrctlRoom";
+        type Type = super::FrctlRoom;
+        type ParentType = glib::Object;
+
+        fn new() -> Self {
+            Self {
+                room: OnceCell::new(),
+                name: RefCell::new(Some("Unknown".to_string())),
+                avatar: RefCell::new(None),
+            }
+        }
+    }
+
+    impl ObjectImpl for FrctlRoom {
+        fn properties() -> &'static [glib::ParamSpec] {
+            use once_cell::sync::Lazy;
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::boxed(
+                        "room",
+                        "Room",
+                        "The matrix room",
+                        BoxedRoom::static_type(),
+                        glib::ParamFlags::WRITABLE | glib::ParamFlags::CONSTRUCT_ONLY,
+                    ),
+                    glib::ParamSpec::string(
+                        "display-name",
+                        "Display Name",
+                        "The display name of this room",
+                        None,
+                        glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpec::object(
+                        "avatar",
+                        "Avatar",
+                        "The url of the avatar of this room",
+                        gio::LoadableIcon::static_type(),
+                        glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpec::flags(
+                        "highlight",
+                        "Highlight",
+                        "How this room is highlighted",
+                        HighlightFlags::static_type(),
+                        HighlightFlags::default().bits(),
+                        glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpec::uint64(
+                        "notification-count",
+                        "Notification count",
+                        "The notification count of this room",
+                        std::u64::MIN,
+                        std::u64::MAX,
+                        0,
+                        glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                ]
+                // TODO: add other needed properties e.g. is_direct, and category
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.get_name() {
+                "room" => {
+                    let room = value
+                        .get_some::<&BoxedRoom>()
+                        .expect("type conformity checked by `Object::set_property`");
+                    let _ = self.room.set(room.clone().0);
+                    obj.update();
+                }
+                _ => unimplemented!(),
+            }
+        }
+
+        fn get_property(
+            &self,
+            _obj: &Self::Type,
+            _id: usize,
+            pspec: &glib::ParamSpec,
+        ) -> glib::Value {
+            let room = self.room.get().unwrap();
+            match pspec.get_name() {
+                "display-name" => self.name.borrow().to_value(),
+                "avatar" => self.avatar.borrow().to_value(),
+                "highlight" => {
+                    let count = room.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
+                    }
+                    .to_value()
+                }
+                "notification-count" => {
+                    let highlight = room.unread_notification_counts().highlight_count;
+                    let notification = room.unread_notification_counts().notification_count;
+
+                    if highlight > 0 {
+                        highlight
+                    } else {
+                        notification
+                    }
+                    .to_value()
+                }
+                _ => unimplemented!(),
+            }
+        }
+    }
+}
+
+glib::wrapper! {
+    pub struct FrctlRoom(ObjectSubclass<imp::FrctlRoom>);
+}
+
+#[derive(Clone, Debug, glib::GBoxed)]
+#[gboxed(type_name = "BoxedRoom")]
+struct BoxedRoom(Room);
+
+impl FrctlRoom {
+    pub fn new(room: &Room) -> Self {
+        glib::Object::new(&[("room", &BoxedRoom(room.clone()))])
+            .expect("Failed to create FrctlRoom")
+    }
+
+    /// This should be called when any field on the Room has changed
+    pub fn update(&self) {
+        self.load_display_name();
+        self.load_avatar();
+        self.notify("highlight");
+        self.notify("notification-count");
+    }
+
+    fn load_display_name(&self) {
+        let obj = self.downgrade();
+        spawn!(async move {
+            if let Some(obj) = obj.upgrade() {
+                let priv_ = imp::FrctlRoom::from_instance(&obj);
+                let name = &priv_.name;
+                let new_name = priv_.room.get().unwrap().display_name().await.ok();
+
+                if *name.borrow() != new_name {
+                    name.replace(new_name);
+                    obj.notify("display-name");
+                }
+            }
+        });
+    }
+
+    fn load_avatar(&self) {
+        // TODO: load avatar and create a LoadableIcon
+    }
+}
diff --git a/src/session/sidebar/room_row.rs b/src/session/sidebar/room_row.rs
new file mode 100644
index 00000000..90a8f911
--- /dev/null
+++ b/src/session/sidebar/room_row.rs
@@ -0,0 +1,164 @@
+use crate::session::sidebar::HighlightFlags;
+use adw;
+use adw::subclass::prelude::BinImpl;
+use gtk::subclass::prelude::*;
+use gtk::{self, prelude::*};
+use gtk::{gio, glib, CompositeTemplate};
+
+mod imp {
+    use super::*;
+    use glib::subclass::InitializingObject;
+
+    #[derive(Debug, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/sidebar-room-row.ui")]
+    pub struct FrctlSidebarRoomRow {
+        #[template_child]
+        pub avatar: TemplateChild<adw::Avatar>,
+        #[template_child]
+        pub display_name: TemplateChild<gtk::Label>,
+        #[template_child]
+        pub notification_count: TemplateChild<gtk::Label>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for FrctlSidebarRoomRow {
+        const NAME: &'static str = "FrctlSidebarRoomRow";
+        type Type = super::FrctlSidebarRoomRow;
+        type ParentType = adw::Bin;
+
+        fn new() -> Self {
+            Self {
+                avatar: TemplateChild::default(),
+                display_name: TemplateChild::default(),
+                notification_count: TemplateChild::default(),
+            }
+        }
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for FrctlSidebarRoomRow {
+        fn properties() -> &'static [glib::ParamSpec] {
+            use once_cell::sync::Lazy;
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::object(
+                        "avatar",
+                        "Avatar",
+                        "The url of the avatar of this room",
+                        gio::LoadableIcon::static_type(),
+                        glib::ParamFlags::WRITABLE,
+                    ),
+                    glib::ParamSpec::string(
+                        "display-name",
+                        "Display Name",
+                        "The display name of this room",
+                        None,
+                        glib::ParamFlags::WRITABLE,
+                    ),
+                    glib::ParamSpec::flags(
+                        "highlight",
+                        "Highlight",
+                        "What type of highligh this room needs",
+                        HighlightFlags::static_type(),
+                        HighlightFlags::default().bits(),
+                        glib::ParamFlags::WRITABLE,
+                    ),
+                    glib::ParamSpec::uint64(
+                        "notification-count",
+                        "Notification count",
+                        "The notification count of this room",
+                        std::u64::MIN,
+                        std::u64::MAX,
+                        0,
+                        glib::ParamFlags::WRITABLE,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            _obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.get_name() {
+                "avatar" => {
+                    let _avatar = value
+                        .get::<gio::LoadableIcon>()
+                        .expect("type conformity checked by `Object::set_property`");
+                    // TODO: set custom avatar https://gitlab.gnome.org/exalm/libadwaita/-/issues/29
+                }
+                "display-name" => {
+                    let display_name = value
+                        .get()
+                        .expect("type conformity checked by `Object::set_property`")
+                        .expect("A room always needs a display name");
+                    self.display_name.set_label(display_name);
+                }
+                "highlight" => {
+                    let flags = value
+                        .get::<HighlightFlags>()
+                        .expect("type conformity checked by `Object::set_property`")
+                        .unwrap();
+                    match flags {
+                        HighlightFlags::NONE => {
+                            self.notification_count.remove_css_class("highlight");
+                            self.display_name.remove_css_class("bold");
+                        }
+                        HighlightFlags::HIGHLIGHT => {
+                            self.notification_count.add_css_class("highlight");
+                            self.display_name.remove_css_class("bold");
+                        }
+                        HighlightFlags::BOLD => {
+                            self.display_name.add_css_class("bold");
+                            self.notification_count.remove_css_class("highlight");
+                        }
+                        HighlightFlags::HIGHLIGHT_BOLD => {
+                            self.notification_count.add_css_class("highlight");
+                            self.display_name.add_css_class("bold");
+                        }
+                        _ => {}
+                    }
+                }
+                "notification-count" => {
+                    let count = value
+                        .get::<u64>()
+                        .expect("type conformity checked by `Object::set_property`")
+                        .unwrap();
+                    self.notification_count.set_label(&count.to_string());
+                    self.notification_count.set_visible(count > 0);
+                }
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+        }
+    }
+
+    impl WidgetImpl for FrctlSidebarRoomRow {}
+    impl BinImpl for FrctlSidebarRoomRow {}
+}
+
+glib::wrapper! {
+    pub struct FrctlSidebarRoomRow(ObjectSubclass<imp::FrctlSidebarRoomRow>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl FrctlSidebarRoomRow {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create FrctlSidebarRoomRow")
+    }
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]