sidebar: Add things needed for the Sidebar

This includes gobject wrappers and the ui.
This commit is contained in:
Julian Sparber 2021-02-23 16:10:45 +01:00 committed by Alejandro Domínguez
parent e4220835ab
commit a22fa19e38
15 changed files with 1165 additions and 10 deletions

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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(

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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")
}
}

View File

@ -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()
}
}

200
src/session/sidebar/room.rs Normal file
View File

@ -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
}
}

View File

@ -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")
}
}