[fractal/fractal-next] content: Add room explore
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] content: Add room explore
- Date: Sat, 12 Jun 2021 14:09:45 +0000 (UTC)
commit ec7720c0fd12ed093cd32ba437a57f4da5870347
Author: Julian Sparber <julian sparber net>
Date: Thu Jun 10 16:53:09 2021 +0200
content: Add room explore
data/resources/resources.gresource.xml | 3 +
data/resources/style.css | 8 +
data/resources/ui/content-explore-item.ui | 14 ++
data/resources/ui/content-explore.ui | 99 ++++++++
data/resources/ui/content-public-room-row.ui | 89 +++++++
data/resources/ui/content.ui | 6 +
data/resources/ui/session.ui | 1 +
src/components/spinner_button.rs | 2 +-
src/meson.build | 5 +
src/session/content/content.rs | 34 ++-
src/session/content/explore/explore.rs | 239 ++++++++++++++++++
src/session/content/explore/mod.rs | 9 +
src/session/content/explore/public_room.rs | 212 ++++++++++++++++
src/session/content/explore/public_room_list.rs | 313 ++++++++++++++++++++++++
src/session/content/explore/public_room_row.rs | 231 +++++++++++++++++
src/session/content/mod.rs | 2 +
src/session/room_list.rs | 133 +++++++++-
17 files changed, 1389 insertions(+), 11 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index ebcba03e..e3fa5bdb 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -4,6 +4,9 @@
<file compressed="true" preprocess="xml-stripblanks" alias="shortcuts.ui">ui/shortcuts.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content.ui">ui/content.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-room-history.ui">ui/content-room-history.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="content-explore.ui">ui/content-explore.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="content-explore-item.ui">ui/content-explore-item.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="content-public-room-row.ui">ui/content-public-room-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-item.ui">ui/content-item.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-item-row-menu.ui">ui/content-item-row-menu.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-message-row.ui">ui/content-message-row.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index ee1a5c59..4b64ba80 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -161,3 +161,11 @@ headerbar.flat {
background-color: @theme_base_color;
padding: 6px;
}
+
+.explore listview {
+ padding: 12px;
+}
+
+.bold {
+ font-weight: bold;
+}
diff --git a/data/resources/ui/content-explore-item.ui b/data/resources/ui/content-explore-item.ui
new file mode 100644
index 00000000..bd83d29e
--- /dev/null
+++ b/data/resources/ui/content-explore-item.ui
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GtkListItem">
+ <property name="activatable">False</property>
+ <property name="selectable">False</property>
+ <property name="child">
+ <object class="ContentPublicRoomRow">
+ <binding name="public-room">
+ <lookup name="item">GtkListItem</lookup>
+ </binding>
+ </object>
+ </property>
+ </template>
+</interface>
diff --git a/data/resources/ui/content-explore.ui b/data/resources/ui/content-explore.ui
new file mode 100644
index 00000000..76dc2fab
--- /dev/null
+++ b/data/resources/ui/content-explore.ui
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ContentExplore" parent="AdwBin">
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="AdwHeaderBar" id="headerbar">
+ <property name="show-start-title-buttons" bind-source="ContentExplore" bind-property="compact"
bind-flags="sync-create"/>
+ <child type="start">
+ <object class="GtkButton" id="back">
+ <property name="visible" bind-source="ContentExplore" bind-property="compact"
bind-flags="sync-create"/>
+ <property name="icon-name">go-previous-symbolic</property>
+ <property name="action-name">content.go-back</property>
+ </object>
+ </child>
+ <child type="title">
+ <object class="AdwClamp">
+ <property name="maximum-size">400</property>
+ <property name="hexpand">True</property>
+ <property name="child">
+ <object class="GtkSearchEntry" id="search_entry">
+ </object>
+ </property>
+ </object>
+ </child>
+ <child type="end">
+ <object class="GtkComboBoxText" id="network_menu">
+ <property name="active-id">matrix</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="visible-child">spinner</property>
+ <property name="transition-type">crossfade</property>
+ <style>
+ <class name="explore"/>
+ </style>
+ <child>
+ <object class="GtkSpinner" id="spinner">
+ <property name="spinning">True</property>
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <property name="vexpand">True</property>
+ <style>
+ <class name="session-loading-spinner"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="empty_label">
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <property name="vexpand">True</property>
+ <property name="label" translatable="yes">No rooms matching the search where found</property>
+ <style>
+ <class name="bold"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="vexpand">True</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="child">
+ <object class="AdwClampScrollable">
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <property name="maximum-size">800</property>
+ <property name="child">
+ <object class="GtkListView" id="listview">
+ <style>
+ <class name="content"/>
+ </style>
+ <property name="factory">
+ <object class="GtkBuilderListItemFactory">
+ <property
name="resource">/org/gnome/FractalNext/content-explore-item.ui</property>
+ </object>
+ </property>
+ <accessibility>
+ <property name="label" translatable="yes">Room List</property>
+ </accessibility>
+ </object>
+ </property>
+ </object>
+ </property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
+
diff --git a/data/resources/ui/content-public-room-row.ui b/data/resources/ui/content-public-room-row.ui
new file mode 100644
index 00000000..ce440919
--- /dev/null
+++ b/data/resources/ui/content-public-room-row.ui
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ContentPublicRoomRow" parent="AdwBin">
+ <property name="child">
+ <object class="GtkBox">
+ <property name="spacing">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <child>
+ <object class="ComponentsAvatar" id="avatar">
+ <property name="size">48</property>
+ <property name="valign">start</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="hexpand">True</property>
+ <property name="halign">start</property>
+ <child>
+ <object class="GtkLabel" id="display_name">
+ <property name="halign">start</property>
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="bold"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="description">
+ <property name="halign">start</property>
+ <property name="ellipsize">end</property>
+ <property name="lines">4</property>
+ <property name="wrap">True</property>
+ <property name="wrap-mode">word-char</property>
+ <property name="xalign">0</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="alias">
+ <property name="ellipsize">end</property>
+ <property name="halign">start</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="end">
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="SpinnerButton" id="button">
+ <property name="valign">center</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="halign">center</property>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">system-users-symbolic</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="members_count">
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </property>
+ </template>
+</interface>
+
diff --git a/data/resources/ui/content.ui b/data/resources/ui/content.ui
index a8c96a95..7c1a8086 100644
--- a/data/resources/ui/content.ui
+++ b/data/resources/ui/content.ui
@@ -25,6 +25,12 @@
<property name="room" bind-source="Content" bind-property="room" bind-flags="sync-create"/>
</object>
</child>
+ <child>
+ <object class="ContentExplore" id="explore">
+ <property name="compact" bind-source="Content" bind-property="compact"
bind-flags="sync-create"/>
+ <property name="session" bind-source="Content" bind-property="session"
bind-flags="sync-create"/>
+ </object>
+ </child>
</object>
</child>
</object>
diff --git a/data/resources/ui/session.ui b/data/resources/ui/session.ui
index 363738c9..7d5f4d59 100644
--- a/data/resources/ui/session.ui
+++ b/data/resources/ui/session.ui
@@ -59,6 +59,7 @@
<property name="compact" bind-source="content" bind-property="folded"
bind-flags="sync-create"/>
<property name="room" bind-source="Session" bind-property="selected-room"
bind-flags="sync-create | bidirectional"/>
<property name="content-type" bind-source="Session"
bind-property="selected-content-type" bind-flags="sync-create | bidirectional"/>
+ <property name="session">Session</property>
<property name="error-list">error_list</property>
</object>
</child>
diff --git a/src/components/spinner_button.rs b/src/components/spinner_button.rs
index 92dcab40..edf0b387 100644
--- a/src/components/spinner_button.rs
+++ b/src/components/spinner_button.rs
@@ -89,7 +89,7 @@ mod imp {
glib::wrapper! {
pub struct SpinnerButton(ObjectSubclass<imp::SpinnerButton>)
- @extends gtk::Widget, gtk::Button, @implements gtk::Accessible;
+ @extends gtk::Widget, gtk::Button, @implements gtk::Accessible, gtk::Actionable;
}
/// A widget displaying a `User`
diff --git a/src/meson.build b/src/meson.build
index d0734274..db9f9599 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -40,6 +40,11 @@ sources = files(
'session/mod.rs',
'session/content/content.rs',
'session/content/divider_row.rs',
+ 'session/content/explore/explore.rs',
+ 'session/content/explore/mod.rs',
+ 'session/content/explore/public_room.rs',
+ 'session/content/explore/public_room_list.rs',
+ 'session/content/explore/public_room_row.rs',
'session/content/item_row.rs',
'session/content/invite.rs',
'session/content/markdown_popover.rs',
diff --git a/src/session/content/content.rs b/src/session/content/content.rs
index ac16a977..74b0eeeb 100644
--- a/src/session/content/content.rs
+++ b/src/session/content/content.rs
@@ -1,8 +1,7 @@
use crate::session::{
- content::ContentType,
- content::Invite,
- content::RoomHistory,
+ content::{ContentType, Explore, Invite, RoomHistory},
room::{Room, RoomType},
+ Session,
};
use adw::subclass::prelude::*;
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
@@ -10,12 +9,14 @@ use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTem
mod imp {
use super::*;
use glib::{signal::SignalHandlerId, subclass::InitializingObject};
+ use once_cell::sync::Lazy;
use std::cell::{Cell, RefCell};
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/FractalNext/content.ui")]
pub struct Content {
pub compact: Cell<bool>,
+ pub session: RefCell<Option<Session>>,
pub room: RefCell<Option<Room>>,
pub content_type: Cell<ContentType>,
pub error_list: RefCell<Option<gio::ListStore>>,
@@ -26,6 +27,8 @@ mod imp {
pub room_history: TemplateChild<RoomHistory>,
#[template_child]
pub invite: TemplateChild<Invite>,
+ #[template_child]
+ pub explore: TemplateChild<Explore>,
}
#[glib::object_subclass]
@@ -37,6 +40,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
RoomHistory::static_type();
Invite::static_type();
+ Explore::static_type();
Self::bind_template(klass);
klass.set_accessible_role(gtk::AccessibleRole::Group);
@@ -52,9 +56,15 @@ mod imp {
impl ObjectImpl for Content {
fn properties() -> &'static [glib::ParamSpec] {
- use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
+ glib::ParamSpec::new_object(
+ "session",
+ "Session",
+ "The session",
+ Session::static_type(),
+ glib::ParamFlags::READWRITE,
+ ),
glib::ParamSpec::new_boolean(
"compact",
"Compact",
@@ -102,6 +112,9 @@ mod imp {
let compact = value.get().unwrap();
self.compact.set(compact);
}
+ "session" => {
+ let _ = self.session.replace(value.get().unwrap());
+ }
"room" => {
let room = value.get().unwrap();
obj.set_room(room);
@@ -117,6 +130,7 @@ mod imp {
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"compact" => self.compact.get().to_value(),
+ "session" => obj.session().to_value(),
"room" => obj.room().to_value(),
"error-list" => self.error_list.borrow().to_value(),
"content-type" => obj.content_type().to_value(),
@@ -135,8 +149,13 @@ glib::wrapper! {
}
impl Content {
- pub fn new() -> Self {
- glib::Object::new(&[]).expect("Failed to create Content")
+ pub fn new(session: &Session) -> Self {
+ glib::Object::new(&[("session", session)]).expect("Failed to create Content")
+ }
+
+ pub fn session(&self) -> Option<Session> {
+ let priv_ = imp::Content::from_instance(self);
+ priv_.session.borrow().to_owned()
}
pub fn content_type(&self) -> ContentType {
@@ -208,7 +227,8 @@ impl Content {
}
}
ContentType::Explore => {
- todo!("Display explore");
+ priv_.explore.init();
+ priv_.stack.set_visible_child(&*priv_.explore);
}
}
}
diff --git a/src/session/content/explore/explore.rs b/src/session/content/explore/explore.rs
new file mode 100644
index 00000000..8d3e3e08
--- /dev/null
+++ b/src/session/content/explore/explore.rs
@@ -0,0 +1,239 @@
+use crate::{
+ session::content::explore::{PublicRoom, PublicRoomList, PublicRoomRow},
+ session::Session,
+};
+
+use matrix_sdk::api::r0::thirdparty::get_protocols;
+
+use crate::utils::do_async;
+use adw::subclass::prelude::*;
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+use log::error;
+
+mod imp {
+ use super::*;
+ use glib::subclass::InitializingObject;
+ use once_cell::sync::Lazy;
+ use std::cell::{Cell, RefCell};
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/content-explore.ui")]
+ pub struct Explore {
+ pub compact: Cell<bool>,
+ pub session: RefCell<Option<Session>>,
+ #[template_child]
+ pub stack: TemplateChild<gtk::Stack>,
+ #[template_child]
+ pub spinner: TemplateChild<gtk::Spinner>,
+ #[template_child]
+ pub empty_label: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub search_entry: TemplateChild<gtk::SearchEntry>,
+ #[template_child]
+ pub network_menu: TemplateChild<gtk::ComboBoxText>,
+ #[template_child]
+ pub listview: TemplateChild<gtk::ListView>,
+ #[template_child]
+ pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
+ pub public_room_list: RefCell<Option<PublicRoomList>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Explore {
+ const NAME: &'static str = "ContentExplore";
+ type Type = super::Explore;
+ type ParentType = adw::Bin;
+
+ fn class_init(klass: &mut Self::Class) {
+ PublicRoom::static_type();
+ PublicRoomList::static_type();
+ PublicRoomRow::static_type();
+ Self::bind_template(klass);
+ klass.set_accessible_role(gtk::AccessibleRole::Group);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for Explore {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_boolean(
+ "compact",
+ "Compact",
+ "Wheter a compact view is used or not",
+ false,
+ glib::ParamFlags::READWRITE,
+ ),
+ glib::ParamSpec::new_object(
+ "session",
+ "Session",
+ "The session",
+ Session::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "compact" => self.compact.set(value.get().unwrap()),
+ "session" => obj.set_session(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "compact" => self.compact.get().to_value(),
+ "session" => obj.session().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+ let adj = self.scrolled_window.vadjustment().unwrap();
+
+ adj.connect_value_changed(clone!(@weak obj => move |adj| {
+ if adj.upper() - adj.value() < adj.page_size() * 2.0 {
+ let priv_ = imp::Explore::from_instance(&obj);
+ if let Some(public_room_list) = &*priv_.public_room_list.borrow() {
+ public_room_list.load_public_rooms(false);
+ }
+ }
+ }));
+
+ self.search_entry
+ .connect_search_changed(clone!(@weak obj => move |_| {
+ let priv_ = imp::Explore::from_instance(&obj);
+ if let Some(public_room_list) = &*priv_.public_room_list.borrow() {
+ let text = priv_.search_entry.text().as_str().to_string();
+ let network = priv_.network_menu.active_id().map(|id| id.as_str().to_owned());
+ public_room_list.search(Some(text), None, network);
+ };
+ }));
+ }
+ }
+
+ impl WidgetImpl for Explore {}
+ impl BinImpl for Explore {}
+}
+
+glib::wrapper! {
+ pub struct Explore(ObjectSubclass<imp::Explore>)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl Explore {
+ pub fn new(session: &Session) -> Self {
+ glib::Object::new(&[("session", session)]).expect("Failed to create Explore")
+ }
+
+ pub fn session(&self) -> Option<Session> {
+ let priv_ = imp::Explore::from_instance(self);
+ priv_.session.borrow().to_owned()
+ }
+
+ pub fn init(&self) {
+ let priv_ = imp::Explore::from_instance(self);
+ self.load_protocols();
+ if let Some(public_room_list) = &*priv_.public_room_list.borrow() {
+ public_room_list.load_public_rooms(true);
+ }
+ }
+
+ pub fn set_session(&self, session: Option<Session>) {
+ let priv_ = imp::Explore::from_instance(self);
+
+ if session == self.session() {
+ return;
+ }
+
+ if let Some(ref session) = session {
+ let public_room_list = PublicRoomList::new(session);
+ priv_
+ .listview
+ .set_model(Some(>k::NoSelection::new(Some(&public_room_list))));
+
+ public_room_list.connect_notify_local(
+ Some("loading"),
+ clone!(@weak self as obj => move |_, _| {
+ obj.set_visible_child();
+ }),
+ );
+
+ public_room_list.connect_notify_local(
+ Some("empty"),
+ clone!(@weak self as obj => move |_, _| {
+ obj.set_visible_child();
+ }),
+ );
+
+ priv_.public_room_list.replace(Some(public_room_list));
+ }
+
+ priv_.session.replace(session);
+ self.notify("session");
+ }
+
+ fn set_visible_child(&self) {
+ let priv_ = imp::Explore::from_instance(self);
+ if let Some(public_room_list) = &*priv_.public_room_list.borrow() {
+ if public_room_list.loading() {
+ priv_.stack.set_visible_child(&*priv_.spinner);
+ } else if public_room_list.empty() {
+ priv_.stack.set_visible_child(&*priv_.empty_label);
+ } else {
+ priv_.stack.set_visible_child(&*priv_.scrolled_window);
+ }
+ }
+ }
+
+ fn set_protocols(&self, protocols: get_protocols::Response) {
+ let priv_ = imp::Explore::from_instance(self);
+
+ for protocol in protocols
+ .protocols
+ .into_iter()
+ .flat_map(|(_, protocol)| protocol.instances)
+ {
+ priv_
+ .network_menu
+ .append(Some(&protocol.instance_id), &protocol.desc);
+ }
+ }
+
+ fn load_protocols(&self) {
+ let priv_ = imp::Explore::from_instance(self);
+ let client = self.session().unwrap().client().clone();
+
+ priv_.network_menu.remove_all();
+ priv_.network_menu.append(Some("matrix"), "Matrix");
+ priv_.network_menu.append(Some("all"), "All rooms");
+ priv_.network_menu.set_active(Some(0));
+
+ do_async(
+ glib::PRIORITY_DEFAULT_IDLE,
+ async move { client.send(get_protocols::Request::new(), None).await },
+ clone!(@weak self as obj => move |result| async move {
+ match result {
+ Ok(response) => obj.set_protocols(response),
+ Err(error) => error!("Error loading supported protocols: {}", error),
+ }
+ }),
+ );
+ }
+}
diff --git a/src/session/content/explore/mod.rs b/src/session/content/explore/mod.rs
new file mode 100644
index 00000000..000b8877
--- /dev/null
+++ b/src/session/content/explore/mod.rs
@@ -0,0 +1,9 @@
+mod explore;
+mod public_room;
+mod public_room_list;
+mod public_room_row;
+
+pub use self::explore::Explore;
+pub use self::public_room::PublicRoom;
+pub use self::public_room_list::PublicRoomList;
+pub use self::public_room_row::PublicRoomRow;
diff --git a/src/session/content/explore/public_room.rs b/src/session/content/explore/public_room.rs
new file mode 100644
index 00000000..6869cf92
--- /dev/null
+++ b/src/session/content/explore/public_room.rs
@@ -0,0 +1,212 @@
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
+use matrix_sdk::directory::PublicRoomsChunk;
+
+use crate::session::{room::Room, Avatar, Session};
+
+mod imp {
+ use super::*;
+ use glib::signal::SignalHandlerId;
+ use once_cell::sync::{Lazy, OnceCell};
+ use std::cell::{Cell, RefCell};
+
+ #[derive(Debug, Default)]
+ pub struct PublicRoom {
+ pub session: OnceCell<Session>,
+ pub matrix_public_room: OnceCell<PublicRoomsChunk>,
+ pub avatar: OnceCell<Avatar>,
+ pub room: OnceCell<Room>,
+ pub is_pending: Cell<bool>,
+ pub room_handler: RefCell<Option<SignalHandlerId>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for PublicRoom {
+ const NAME: &'static str = "PublicRoom";
+ type Type = super::PublicRoom;
+ type ParentType = glib::Object;
+ }
+
+ impl ObjectImpl for PublicRoom {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_object(
+ "session",
+ "Session",
+ "The session",
+ Session::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ glib::ParamSpec::new_object(
+ "room",
+ "Room",
+ "The room, this is only set if the user is alerady a member",
+ Room::static_type(),
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpec::new_boolean(
+ "pending",
+ "Pending",
+ "A room is pending when the user already clicked to join a room",
+ false,
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpec::new_object(
+ "avatar",
+ "Avatar",
+ "The Avatar of this room",
+ Avatar::static_type(),
+ glib::ParamFlags::READABLE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ _obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "session" => self.session.set(value.get().unwrap()).unwrap(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "session" => obj.session().to_value(),
+ "avatar" => obj.avatar().to_value(),
+ "room" => obj.room().to_value(),
+ "pending" => obj.is_pending().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+
+ self.avatar.set(Avatar::new(obj.session(), None)).unwrap();
+
+ obj.session()
+ .room_list()
+ .connect_pending_rooms_changed(clone!(@weak obj => move |_| {
+ if let Some(matrix_public_room) = obj.matrix_public_room() {
+ obj.set_pending(obj.session()
+ .room_list()
+ .is_pending_room(&matrix_public_room.room_id.clone().into()));
+ }
+ }));
+ }
+
+ fn dispose(&self, obj: &Self::Type) {
+ if let Some(handler_id) = self.room_handler.take() {
+ obj.session().room_list().disconnect(handler_id);
+ }
+ }
+ }
+}
+
+glib::wrapper! {
+ pub struct PublicRoom(ObjectSubclass<imp::PublicRoom>);
+}
+
+impl PublicRoom {
+ pub fn new(session: &Session) -> Self {
+ glib::Object::new(&[("session", session)]).expect("Failed to create Room")
+ }
+
+ pub fn session(&self) -> &Session {
+ let priv_ = imp::PublicRoom::from_instance(&self);
+ priv_.session.get().unwrap()
+ }
+
+ pub fn avatar(&self) -> &Avatar {
+ let priv_ = imp::PublicRoom::from_instance(self);
+ priv_.avatar.get().unwrap()
+ }
+
+ /// The room if the user is already a member of this room.
+ pub fn room(&self) -> Option<&Room> {
+ let priv_ = imp::PublicRoom::from_instance(self);
+ priv_.room.get()
+ }
+
+ fn set_room(&self, room: Room) {
+ let priv_ = imp::PublicRoom::from_instance(self);
+ priv_.room.set(room).unwrap();
+ self.notify("room");
+ }
+
+ fn set_pending(&self, is_pending: bool) {
+ let priv_ = imp::PublicRoom::from_instance(self);
+
+ if self.is_pending() == is_pending {
+ return;
+ }
+
+ priv_.is_pending.set(is_pending);
+ self.notify("pending");
+ }
+
+ pub fn is_pending(&self) -> bool {
+ let priv_ = imp::PublicRoom::from_instance(self);
+ priv_.is_pending.get()
+ }
+
+ pub fn set_matrix_public_room(&self, room: PublicRoomsChunk) {
+ let priv_ = imp::PublicRoom::from_instance(self);
+
+ self.avatar().set_display_name(room.name.clone());
+ self.avatar().set_url(room.avatar_url.clone());
+
+ if let Some(room) = self.session().room_list().get(&room.room_id) {
+ self.set_room(room);
+ } else {
+ let room_id = room.room_id.clone();
+ let handler_id = self.session().room_list().connect_items_changed(
+ clone!(@weak self as obj => move |room_list, _, _, _| {
+ if let Some(room) = room_list.get(&room_id) {
+ let priv_ = imp::PublicRoom::from_instance(&obj);
+ if let Some(handler_id) = priv_.room_handler.take() {
+ obj.set_room(room);
+ room_list.disconnect(handler_id);
+ }
+ }
+ }),
+ );
+
+ priv_.room_handler.replace(Some(handler_id));
+ }
+
+ self.set_pending(
+ self.session()
+ .room_list()
+ .is_pending_room(&room.room_id.clone().into()),
+ );
+
+ priv_.matrix_public_room.set(room).unwrap();
+ }
+
+ pub fn matrix_public_room(&self) -> Option<&PublicRoomsChunk> {
+ let priv_ = imp::PublicRoom::from_instance(self);
+ priv_.matrix_public_room.get()
+ }
+
+ pub fn join_or_view(&self) {
+ let session = self.session();
+ if let Some(room) = self.room() {
+ session.set_selected_room(Some(room.clone()));
+ } else {
+ if let Some(matrix_public_room) = self.matrix_public_room() {
+ session
+ .room_list()
+ .join_by_id_or_alias(matrix_public_room.room_id.clone().into());
+ }
+ }
+ }
+}
diff --git a/src/session/content/explore/public_room_list.rs b/src/session/content/explore/public_room_list.rs
new file mode 100644
index 00000000..2a63e0f6
--- /dev/null
+++ b/src/session/content/explore/public_room_list.rs
@@ -0,0 +1,313 @@
+use crate::{
+ session::{content::explore::PublicRoom, Session},
+ utils::do_async,
+};
+use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
+use log::error;
+use matrix_sdk::{
+ api::r0::directory::{
+ get_public_rooms_filtered::Request as PublicRoomsRequest,
+ get_public_rooms_filtered::Response as PublicRoomsResponse,
+ },
+ assign,
+ directory::{Filter, RoomNetwork},
+ identifiers::ServerNameBox,
+ uint,
+};
+use std::convert::TryFrom;
+
+mod imp {
+ use once_cell::sync::Lazy;
+ use std::cell::{Cell, RefCell};
+
+ use super::*;
+
+ #[derive(Debug, Default)]
+ pub struct PublicRoomList {
+ pub list: RefCell<Vec<PublicRoom>>,
+ pub search_term: RefCell<Option<String>>,
+ pub network: RefCell<Option<String>>,
+ pub server: RefCell<Option<String>>,
+ pub next_batch: RefCell<Option<String>>,
+ pub loading: Cell<bool>,
+ pub request_sent: Cell<bool>,
+ pub total_room_count_estimate: Cell<Option<u64>>,
+ pub session: RefCell<Option<Session>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for PublicRoomList {
+ const NAME: &'static str = "PublicRoomList";
+ type Type = super::PublicRoomList;
+ type ParentType = glib::Object;
+ type Interfaces = (gio::ListModel,);
+ }
+
+ impl ObjectImpl for PublicRoomList {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_object(
+ "session",
+ "Session",
+ "The session",
+ Session::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ glib::ParamSpec::new_boolean(
+ "loading",
+ "Loading",
+ "Whether a response is loaded or not",
+ false,
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpec::new_boolean(
+ "empty",
+ "Empty",
+ "Whether matching rooms are found or not",
+ false,
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpec::new_boolean(
+ "complete",
+ "Complete",
+ "Whether the every search result is loaded or not",
+ false,
+ glib::ParamFlags::READABLE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ _obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "session" => {
+ let _ = self.session.replace(value.get().unwrap());
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "session" => obj.session().to_value(),
+ "loading" => obj.loading().to_value(),
+ "empty" => obj.empty().to_value(),
+ "complete" => obj.complete().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+
+ impl ListModelImpl for PublicRoomList {
+ fn item_type(&self, _list_model: &Self::Type) -> glib::Type {
+ PublicRoom::static_type()
+ }
+ fn n_items(&self, _list_model: &Self::Type) -> u32 {
+ self.list.borrow().len() as u32
+ }
+ fn item(&self, _list_model: &Self::Type, position: u32) -> Option<glib::Object> {
+ self.list
+ .borrow()
+ .get(position as usize)
+ .map(glib::object::Cast::upcast_ref::<glib::Object>)
+ .cloned()
+ }
+ }
+}
+
+glib::wrapper! {
+ pub struct PublicRoomList(ObjectSubclass<imp::PublicRoomList>)
+ @implements gio::ListModel;
+}
+
+impl PublicRoomList {
+ pub fn new(session: &Session) -> Self {
+ glib::Object::new(&[("session", session)]).expect("Failed to create PublicRoomList")
+ }
+
+ pub fn session(&self) -> Option<Session> {
+ let priv_ = imp::PublicRoomList::from_instance(self);
+ priv_.session.borrow().to_owned()
+ }
+
+ pub fn loading(&self) -> bool {
+ let priv_ = imp::PublicRoomList::from_instance(self);
+ self.request_sent() && priv_.list.borrow().is_empty()
+ }
+
+ pub fn empty(&self) -> bool {
+ let priv_ = imp::PublicRoomList::from_instance(self);
+ !self.request_sent() && priv_.list.borrow().is_empty()
+ }
+
+ pub fn complete(&self) -> bool {
+ let priv_ = imp::PublicRoomList::from_instance(self);
+ priv_.next_batch.borrow().is_none()
+ }
+
+ fn request_sent(&self) -> bool {
+ let priv_ = imp::PublicRoomList::from_instance(self);
+ priv_.request_sent.get()
+ }
+
+ fn set_request_sent(&self, request_sent: bool) {
+ let priv_ = imp::PublicRoomList::from_instance(self);
+ priv_.request_sent.set(request_sent);
+
+ self.notify("loading");
+ self.notify("empty");
+ self.notify("complete");
+ }
+
+ pub fn search(
+ &self,
+ search_term: Option<String>,
+ server: Option<String>,
+ network: Option<String>,
+ ) {
+ let priv_ = imp::PublicRoomList::from_instance(&self);
+
+ if priv_.search_term.borrow().as_ref() == search_term.as_ref()
+ && priv_.server.borrow().as_ref() == server.as_ref()
+ && priv_.network.borrow().as_ref() == network.as_ref()
+ {
+ return;
+ }
+
+ priv_.search_term.replace(search_term);
+ priv_.server.replace(server);
+ priv_.network.replace(network);
+ self.load_public_rooms(true);
+ }
+
+ fn handle_public_rooms_response(&self, response: PublicRoomsResponse) {
+ let priv_ = imp::PublicRoomList::from_instance(&self);
+ let session = &self.session().unwrap();
+
+ priv_.next_batch.replace(response.next_batch.to_owned());
+ priv_
+ .total_room_count_estimate
+ .replace(response.total_room_count_estimate.map(Into::into));
+
+ let (position, removed, added) = {
+ let mut list = priv_.list.borrow_mut();
+ let position = list.len();
+ let added = response.chunk.len();
+ let mut new_rooms = response
+ .chunk
+ .into_iter()
+ .map(|matrix_room| {
+ let room = PublicRoom::new(session);
+ room.set_matrix_public_room(matrix_room);
+ room
+ })
+ .collect();
+
+ let empty_row = list.pop().unwrap_or(PublicRoom::new(session));
+ list.append(&mut new_rooms);
+
+ if !self.complete() {
+ list.push(empty_row);
+ if position == 0 {
+ (position, 0, added + 1)
+ } else {
+ (position - 1, 0, added)
+ }
+ } else {
+ (position, 1, added)
+ }
+ };
+
+ if added > 0 {
+ self.items_changed(position as u32, removed as u32, added as u32);
+ }
+ self.set_request_sent(false);
+ }
+
+ fn is_valid_response(
+ &self,
+ search_term: Option<String>,
+ server: Option<String>,
+ network: Option<String>,
+ ) -> bool {
+ let priv_ = imp::PublicRoomList::from_instance(self);
+ priv_.search_term.borrow().as_ref() == search_term.as_ref()
+ && priv_.server.borrow().as_ref() == server.as_ref()
+ && priv_.network.borrow().as_ref() == network.as_ref()
+ }
+
+ pub fn load_public_rooms(&self, clear: bool) {
+ let priv_ = imp::PublicRoomList::from_instance(self);
+
+ if self.request_sent() && !clear {
+ return;
+ }
+
+ if clear {
+ // Clear the previous list
+ let removed = priv_.list.borrow().len();
+ priv_.list.borrow_mut().clear();
+ let _ = priv_.next_batch.take();
+ self.items_changed(0, removed as u32, 0);
+ }
+
+ self.set_request_sent(true);
+
+ let next_batch = priv_.next_batch.borrow().clone();
+
+ if next_batch.is_none() && !clear {
+ return;
+ }
+
+ let client = self.session().unwrap().client().clone();
+ let search_term = priv_.search_term.borrow().to_owned();
+ let server = priv_.server.borrow().to_owned();
+ let network = priv_.network.borrow().to_owned();
+ let current_search_term = search_term.clone();
+ let current_server = server.clone();
+ let current_network = network.clone();
+
+ do_async(
+ glib::PRIORITY_DEFAULT_IDLE,
+ async move {
+ let room_network = match network.as_deref() {
+ Some("matrix") => RoomNetwork::Matrix,
+ Some("all") => RoomNetwork::All,
+ Some(custom) => RoomNetwork::ThirdParty(custom),
+ _ => RoomNetwork::default(),
+ };
+ let server = server.and_then(|server| ServerNameBox::try_from(server).ok());
+
+ let request = assign!(PublicRoomsRequest::new(), {
+ limit: Some(uint!(20)),
+ since: next_batch.as_deref(),
+ room_network,
+ server: server.as_deref(),
+ filter: assign!(Filter::new(), { generic_search_term: search_term.as_deref() }),
+ });
+ client.public_rooms_filtered(request).await
+ },
+ clone!(@weak self as obj => move |result| async move {
+ // If the search term changed we ignore the response
+ if obj.is_valid_response(current_search_term, current_server, current_network) {
+ match result {
+ Ok(response) => obj.handle_public_rooms_response(response),
+ Err(error) => {
+ obj.set_request_sent(false);
+ error!("Error loading public rooms: {}", error)
+ },
+ }
+ }
+ }),
+ );
+ }
+}
diff --git a/src/session/content/explore/public_room_row.rs b/src/session/content/explore/public_room_row.rs
new file mode 100644
index 00000000..2027b1ad
--- /dev/null
+++ b/src/session/content/explore/public_room_row.rs
@@ -0,0 +1,231 @@
+use crate::components::Avatar;
+use crate::{components::SpinnerButton, session::content::explore::PublicRoom};
+use adw::prelude::BinExt;
+use adw::subclass::prelude::BinImpl;
+use gettextrs::gettext;
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+mod imp {
+ use super::*;
+ use glib::{signal::SignalHandlerId, subclass::InitializingObject};
+ use once_cell::sync::Lazy;
+ use std::cell::RefCell;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/content-public-room-row.ui")]
+ pub struct PublicRoomRow {
+ pub public_room: RefCell<Option<PublicRoom>>,
+ #[template_child]
+ pub avatar: TemplateChild<Avatar>,
+ #[template_child]
+ pub display_name: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub description: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub alias: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub members_count: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub button: TemplateChild<SpinnerButton>,
+ pub original_child: RefCell<Option<gtk::Widget>>,
+ pub pending_handler: RefCell<Option<SignalHandlerId>>,
+ pub room_handler: RefCell<Option<SignalHandlerId>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for PublicRoomRow {
+ const NAME: &'static str = "ContentPublicRoomRow";
+ type Type = super::PublicRoomRow;
+ type ParentType = adw::Bin;
+
+ fn class_init(klass: &mut Self::Class) {
+ Avatar::static_type();
+ Self::bind_template(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for PublicRoomRow {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpec::new_object(
+ "public-room",
+ "Public Room",
+ "The public room displayed by this row",
+ PublicRoom::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "public-room" => obj.set_public_room(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "public-room" => obj.public_room().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+ self.button.connect_clicked(clone!(@weak obj => move |_| {
+ let priv_ = imp::PublicRoomRow::from_instance(&obj);
+ if let Some(public_room) = &*priv_.public_room.borrow() {
+ public_room.join_or_view();
+ };
+ }));
+ }
+
+ fn dispose(&self, obj: &Self::Type) {
+ if let Some(ref old_public_room) = obj.public_room() {
+ if let Some(handler) = self.pending_handler.take() {
+ old_public_room.disconnect(handler);
+ }
+ if let Some(handler_id) = self.room_handler.take() {
+ old_public_room.disconnect(handler_id);
+ }
+ }
+ }
+ }
+
+ impl WidgetImpl for PublicRoomRow {}
+ impl BinImpl for PublicRoomRow {}
+}
+
+glib::wrapper! {
+ pub struct PublicRoomRow(ObjectSubclass<imp::PublicRoomRow>)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl PublicRoomRow {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create PublicRoomRow")
+ }
+
+ pub fn public_room(&self) -> Option<PublicRoom> {
+ let priv_ = imp::PublicRoomRow::from_instance(&self);
+ priv_.public_room.borrow().clone()
+ }
+
+ pub fn set_public_room(&self, public_room: Option<PublicRoom>) {
+ let priv_ = imp::PublicRoomRow::from_instance(&self);
+ let old_public_room = self.public_room();
+
+ if old_public_room == public_room {
+ return;
+ }
+
+ if let Some(ref old_public_room) = old_public_room {
+ if let Some(handler) = priv_.room_handler.take() {
+ old_public_room.disconnect(handler);
+ }
+ if let Some(handler) = priv_.pending_handler.take() {
+ old_public_room.disconnect(handler);
+ }
+ }
+
+ if let Some(ref public_room) = public_room {
+ if let Some(child) = priv_.original_child.take() {
+ self.set_child(Some(&child));
+ }
+ if let Some(matrix_public_room) = public_room.matrix_public_room() {
+ priv_
+ .avatar
+ .set_item(Some(public_room.avatar().clone().upcast()));
+
+ if let Some(ref name) = matrix_public_room.name {
+ priv_.display_name.set_text(name);
+ } else {
+ // FIXME: display some other identification for this room
+ priv_.display_name.set_text("Room without name");
+ }
+
+ let has_topic = if let Some(ref topic) = matrix_public_room.topic {
+ priv_.description.set_text(topic);
+ true
+ } else {
+ false
+ };
+
+ priv_.description.set_visible(has_topic);
+
+ let has_alias = if let Some(ref alias) = matrix_public_room.canonical_alias {
+ priv_.alias.set_text(alias.as_str());
+ true
+ } else if let Some(ref alias) = matrix_public_room.aliases.get(0) {
+ priv_.alias.set_text(&alias.as_str());
+ true
+ } else {
+ false
+ };
+
+ priv_.alias.set_visible(has_alias);
+ priv_
+ .members_count
+ .set_text(&matrix_public_room.num_joined_members.to_string());
+
+ let pending_handler = public_room.connect_notify_local(
+ Some("pending"),
+ clone!(@weak self as obj => move |public_room, _| {
+ obj.update_button(public_room);
+ }),
+ );
+
+ priv_.pending_handler.replace(Some(pending_handler));
+
+ let room_handler = public_room.connect_notify_local(
+ Some("room"),
+ clone!(@weak self as obj => move |public_room, _| {
+ obj.update_button(public_room);
+ }),
+ );
+
+ priv_.room_handler.replace(Some(room_handler));
+
+ self.update_button(public_room);
+ } else {
+ if priv_.original_child.borrow().is_none() {
+ let spinner = gtk::SpinnerBuilder::new()
+ .spinning(true)
+ .margin_top(12)
+ .margin_bottom(12)
+ .build();
+ priv_.original_child.replace(self.child());
+ self.set_child(Some(&spinner));
+ }
+ }
+ }
+ priv_
+ .avatar
+ .set_item(public_room.clone().map(|room| room.avatar().clone()));
+ priv_.public_room.replace(public_room);
+ self.notify("public-room");
+ }
+
+ fn update_button(&self, public_room: &PublicRoom) {
+ let priv_ = imp::PublicRoomRow::from_instance(&self);
+ if public_room.room().is_some() {
+ priv_.button.set_label(&gettext("View"));
+ } else {
+ priv_.button.set_label(&gettext("Join"));
+ }
+
+ priv_.button.set_loading(public_room.is_pending());
+ }
+}
diff --git a/src/session/content/mod.rs b/src/session/content/mod.rs
index 0de3f250..1b59348d 100644
--- a/src/session/content/mod.rs
+++ b/src/session/content/mod.rs
@@ -1,6 +1,7 @@
mod content;
mod content_type;
mod divider_row;
+mod explore;
mod invite;
mod item_row;
mod markdown_popover;
@@ -11,6 +12,7 @@ mod state_row;
pub use self::content::Content;
pub use self::content_type::ContentType;
use self::divider_row::DividerRow;
+use self::explore::Explore;
use self::invite::Invite;
use self::item_row::ItemRow;
use self::markdown_popover::MarkdownPopover;
diff --git a/src/session/room_list.rs b/src/session/room_list.rs
index 51228741..c3d1f739 100644
--- a/src/session/room_list.rs
+++ b/src/session/room_list.rs
@@ -1,10 +1,22 @@
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
use indexmap::map::IndexMap;
-use matrix_sdk::{deserialized_responses::Rooms as ResponseRooms, identifiers::RoomId};
-
-use crate::session::{room::Room, Session};
+use matrix_sdk::{
+ deserialized_responses::Rooms as ResponseRooms,
+ identifiers::{RoomId, RoomIdOrAliasId},
+};
+
+use crate::{
+ session::{room::Room, Session},
+ utils::do_async,
+ Error,
+};
+use gettextrs::gettext;
+use log::error;
+use std::cell::Cell;
+use std::collections::HashSet;
mod imp {
+ use glib::subclass::Signal;
use once_cell::sync::{Lazy, OnceCell};
use std::cell::RefCell;
@@ -13,6 +25,7 @@ mod imp {
#[derive(Debug, Default)]
pub struct RoomList {
pub list: RefCell<IndexMap<RoomId, Room>>,
+ pub pending_rooms: RefCell<HashSet<RoomIdOrAliasId>>,
pub session: OnceCell<Session>,
}
@@ -58,6 +71,16 @@ mod imp {
_ => unimplemented!(),
}
}
+
+ fn signals() -> &'static [Signal] {
+ static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
+ vec![
+ Signal::builder("pending-rooms-changed", &[], <()>::static_type().into())
+ .build(),
+ ]
+ });
+ SIGNALS.as_ref()
+ }
}
impl ListModelImpl for RoomList {
@@ -93,11 +116,64 @@ impl RoomList {
priv_.session.get().unwrap()
}
+ pub fn is_pending_room(&self, identifier: &RoomIdOrAliasId) -> bool {
+ let priv_ = imp::RoomList::from_instance(&self);
+ priv_.pending_rooms.borrow().contains(identifier)
+ }
+
+ fn pending_rooms_remove(&self, identifier: &RoomIdOrAliasId) {
+ let priv_ = imp::RoomList::from_instance(&self);
+ priv_.pending_rooms.borrow_mut().remove(identifier);
+ self.emit_by_name("pending-rooms-changed", &[]).unwrap();
+ }
+
+ fn pending_rooms_insert(&self, identifier: RoomIdOrAliasId) {
+ let priv_ = imp::RoomList::from_instance(&self);
+ priv_.pending_rooms.borrow_mut().insert(identifier);
+ self.emit_by_name("pending-rooms-changed", &[]).unwrap();
+ }
+
+ fn pending_rooms_replace_or_remove(&self, identifier: &RoomIdOrAliasId, room_id: RoomId) {
+ let priv_ = imp::RoomList::from_instance(&self);
+ {
+ let mut pending_rooms = priv_.pending_rooms.borrow_mut();
+ pending_rooms.remove(identifier);
+ if !self.contains_key(&room_id) {
+ pending_rooms.insert(room_id.into());
+ }
+ }
+ self.emit_by_name("pending-rooms-changed", &[]).unwrap();
+ }
+
pub fn get(&self, room_id: &RoomId) -> Option<Room> {
let priv_ = imp::RoomList::from_instance(&self);
priv_.list.borrow().get(room_id).cloned()
}
+ /// Waits till the Room becomes available
+ pub async fn get_wait(&self, room_id: RoomId) -> Option<Room> {
+ let priv_ = imp::RoomList::from_instance(&self);
+ if let Some(room) = priv_.list.borrow().get(&room_id) {
+ Some(room.clone())
+ } else {
+ let (sender, receiver) = futures::channel::oneshot::channel();
+
+ let sender = Cell::new(Some(sender));
+ // FIXME: add a timeout
+ let handler_id = self.connect_items_changed(move |obj, _, _, _| {
+ if let Some(room) = obj.get(&room_id) {
+ if let Some(sender) = sender.take() {
+ sender.send(Some(room)).unwrap();
+ }
+ }
+ });
+
+ let room = receiver.await.unwrap();
+ self.disconnect(handler_id);
+ room
+ }
+ }
+
fn get_full(&self, room_id: &RoomId) -> Option<(usize, RoomId, Room)> {
let priv_ = imp::RoomList::from_instance(&self);
priv_
@@ -189,6 +265,7 @@ impl RoomList {
})
.clone();
+ self.pending_rooms_remove(&room_id.into());
room.handle_left_response(left_room);
}
@@ -203,6 +280,7 @@ impl RoomList {
})
.clone();
+ self.pending_rooms_remove(&room_id.into());
room.handle_joined_response(joined_room);
}
@@ -217,6 +295,7 @@ impl RoomList {
})
.clone();
+ self.pending_rooms_remove(&room_id.into());
room.handle_invited_response(invited_room);
}
@@ -224,4 +303,52 @@ impl RoomList {
self.items_added(added);
}
}
+
+ pub fn join_by_id_or_alias(&self, identifier: RoomIdOrAliasId) {
+ let client = self.session().client().clone();
+ let identifier_clone = identifier.clone();
+
+ self.pending_rooms_insert(identifier.clone());
+
+ do_async(
+ glib::PRIORITY_DEFAULT_IDLE,
+ async move {
+ client
+ .join_room_by_id_or_alias(&identifier_clone, &[])
+ .await
+ },
+ clone!(@weak self as obj => move |response| async move {
+ match response {
+ Ok(response) => obj.pending_rooms_replace_or_remove(&identifier, response.room_id),
+ Err(error) => {
+ obj.pending_rooms_remove(&identifier);
+ error!("Joining room {} failed: {}", identifier, error);
+ let error = Error::new(
+ error,
+ clone!(@strong obj => move |_| {
+ let error_message = gettext(format!("Failed to join room {}. Try again
later.", identifier));
+ let error_label =
gtk::LabelBuilder::new().label(&error_message).wrap(true).build();
+ Some(error_label.upcast())
+ }),
+ );
+ obj.session().append_error(&error);
+ }
+ }
+ }),
+ );
+ }
+
+ pub fn connect_pending_rooms_changed<F: Fn(&Self) + 'static>(
+ &self,
+ f: F,
+ ) -> glib::SignalHandlerId {
+ self.connect_local("pending-rooms-changed", true, move |values| {
+ let obj = values[0].get::<Self>().unwrap();
+
+ f(&obj);
+
+ None
+ })
+ .unwrap()
+ }
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]