[fractal/fractal-next] sidebar: Add things needed for the Sidebar
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] sidebar: Add things needed for the Sidebar
- Date: Sat, 27 Mar 2021 18:13:18 +0000 (UTC)
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]