[fractal] explore: Allow to explore custom matrix servers
- From: Kévin Commaille <kcommaille src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal] explore: Allow to explore custom matrix servers
- Date: Wed, 5 Oct 2022 15:53:41 +0000 (UTC)
commit 0232d98dc1813a97acfc785b9bc3525d201abd46
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Sun Sep 25 17:31:35 2022 +0200
explore: Allow to explore custom matrix servers
data/org.gnome.Fractal.gschema.xml.in | 5 +
data/resources/resources.gresource.xml | 2 +
data/resources/style.css | 19 ++
data/resources/ui/content-explore-server-row.ui | 37 ++++
.../ui/content-explore-servers-popover.ui | 51 +++++
data/resources/ui/content-explore.ui | 12 +-
po/POTFILES.in | 1 +
src/secret.rs | 13 +-
src/session/content/explore/mod.rs | 79 +++----
src/session/content/explore/public_room_list.rs | 18 +-
src/session/content/explore/server.rs | 149 +++++++++++++
src/session/content/explore/server_list.rs | 209 ++++++++++++++++++
src/session/content/explore/server_row.rs | 98 +++++++++
src/session/content/explore/servers_popover.rs | 243 +++++++++++++++++++++
src/session/mod.rs | 21 ++
src/session/settings.rs | 183 ++++++++++++++++
16 files changed, 1081 insertions(+), 59 deletions(-)
---
diff --git a/data/org.gnome.Fractal.gschema.xml.in b/data/org.gnome.Fractal.gschema.xml.in
index 3a843448c..bd59bfbbb 100644
--- a/data/org.gnome.Fractal.gschema.xml.in
+++ b/data/org.gnome.Fractal.gschema.xml.in
@@ -18,5 +18,10 @@
<summary>Enable markdown formatting</summary>
<description>Whether messages should be processed as markdown when sending them</description>
</key>
+ <key name="sessions" type="s">
+ <default>'[]'</default>
+ <summary>Session settings</summary>
+ <description>Serialized list of settings per session</description>
+ </key>
</schema>
</schemalist>
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 817437260..58d0a9fb1 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -76,6 +76,8 @@
<file compressed="true" preprocess="xml-stripblanks"
alias="content-completion-row.ui">ui/content-completion-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-divider-row.ui">ui/content-divider-row.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-explore-server-row.ui">ui/content-explore-server-row.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="content-explore-servers-popover.ui">ui/content-explore-servers-popover.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-invite-subpage.ui">ui/content-invite-subpage.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-invite.ui">ui/content-invite.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 1e2974c21..5131931c7 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -549,6 +549,7 @@ message-reactions .reaction-count {
/* Explore */
+
.explore listview {
background: transparent;
}
@@ -557,6 +558,24 @@ message-reactions .reaction-count {
min-width: 64px;
}
+.explore-servers-popover list {
+ background-color: transparent;
+ color: inherit;
+}
+
+.explore-servers-popover list row {
+ min-height: 36px;
+ padding: 0 8px;
+ border-radius: 6px;
+ margin: 0 0 2px;
+}
+
+.explore-servers-popover list row button {
+ min-height: 24px;
+ min-width: 24px;
+}
+
+
/* Invite */
.invite-room-name {
diff --git a/data/resources/ui/content-explore-server-row.ui b/data/resources/ui/content-explore-server-row.ui
new file mode 100644
index 000000000..e9fd987c6
--- /dev/null
+++ b/data/resources/ui/content-explore-server-row.ui
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ExploreServerRow" parent="GtkListBoxRow">
+ <property name="child">
+ <object class="GtkBox">
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="xalign">0.0</property>
+ <property name="hexpand">True</property>
+ <binding name="label">
+ <lookup name="name" type="Server">
+ <lookup name="server">ExploreServerRow</lookup>
+ </lookup>
+ </binding>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="remove_button">
+ <binding name="visible">
+ <lookup name="deletable" type="Server">
+ <lookup name="server">ExploreServerRow</lookup>
+ </lookup>
+ </binding>
+ <property name="icon-name">window-close-symbolic</property>
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <style>
+ <class name="circular"/>
+ <class name="flat"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </property>
+ </template>
+</interface>
diff --git a/data/resources/ui/content-explore-servers-popover.ui
b/data/resources/ui/content-explore-servers-popover.ui
new file mode 100644
index 000000000..cdaa57798
--- /dev/null
+++ b/data/resources/ui/content-explore-servers-popover.ui
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ContentExploreServersPopover" parent="GtkPopover">
+ <property name="has-arrow">false</property>
+ <property name="position">bottom</property>
+ <property name="width-request">260</property>
+ <property name="child">
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="propagate-natural-height">true</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="max-content-height">280</property>
+ <property name="child">
+ <object class="GtkBox">
+ <style>
+ <class name="explore-servers-popover"/>
+ </style>
+ <property name="orientation">vertical</property>
+ <property name="spacing">8</property>
+ <child>
+ <object class="GtkListBox" id="listbox">
+ <property name="selection-mode">browse</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator"/>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <style>
+ <class name="linked"/>
+ </style>
+ <child>
+ <object class="GtkEntry" id="server_entry">
+ <property name="hexpand">true</property>
+ <property name="placeholder-text" translatable="yes">Add server…</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="action-name">explore-servers-popover.add-server</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </property>
+ </object>
+ </property>
+ </template>
+</interface>
diff --git a/data/resources/ui/content-explore.ui b/data/resources/ui/content-explore.ui
index ffa4504c0..f07ed24b9 100644
--- a/data/resources/ui/content-explore.ui
+++ b/data/resources/ui/content-explore.ui
@@ -32,8 +32,15 @@
</object>
</child>
<child type="end">
- <object class="GtkComboBoxText" id="network_menu">
- <property name="active-id">matrix</property>
+ <object class="GtkMenuButton" id="servers_button">
+ <property name="valign">center</property>
+ <property name="direction">down</property>
+ <property name="icon-name">format-justify-left-symbolic</property>
+ <property name="popover">
+ <object class="ContentExploreServersPopover" id="servers_popover">
+ <property name="session" bind-source="ContentExplore" bind-property="session"
bind-flags="sync-create"/>
+ </object>
+ </property>
</object>
</child>
</object>
@@ -101,4 +108,3 @@
</child>
</template>
</interface>
-
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 73ed8c11d..78987dfa3 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -16,6 +16,7 @@ data/resources/ui/account-settings.ui
data/resources/ui/attachment-dialog.ui
data/resources/ui/components-auth-dialog.ui
data/resources/ui/components-loading-listbox-row.ui
+data/resources/ui/content-explore-servers-popover.ui
data/resources/ui/content-explore.ui
data/resources/ui/content-invite-subpage.ui
data/resources/ui/content-invite.ui
diff --git a/src/secret.rs b/src/secret.rs
index 89fdfe3af..e8f5f124b 100644
--- a/src/secret.rs
+++ b/src/secret.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashMap, fmt, path::PathBuf, string::FromUtf8Error};
+use std::{collections::HashMap, ffi::OsStr, fmt, path::PathBuf, string::FromUtf8Error};
use gettextrs::gettext;
use gtk::{gio, glib};
@@ -186,6 +186,17 @@ impl StoredSession {
(attributes, secret)
}
+
+ /// Get the unique ID for this `StoredSession`.
+ ///
+ /// This is the name of the folder where the DB is stored.
+ pub fn id(&self) -> &str {
+ self.path
+ .iter()
+ .next_back()
+ .and_then(OsStr::to_str)
+ .unwrap()
+ }
}
/// A possible error value when converting a `Secret` from a UTF-8 byte vector.
diff --git a/src/session/content/explore/mod.rs b/src/session/content/explore/mod.rs
index 4b94dc16d..a334a1a68 100644
--- a/src/session/content/explore/mod.rs
+++ b/src/session/content/explore/mod.rs
@@ -1,16 +1,19 @@
mod public_room;
mod public_room_list;
mod public_room_row;
+mod server;
+mod server_list;
+mod server_row;
+mod servers_popover;
use adw::subclass::prelude::*;
use gtk::{glib, glib::clone, prelude::*, CompositeTemplate};
-use log::error;
-use ruma::api::client::thirdparty::get_protocols;
pub use self::{
public_room::PublicRoom, public_room_list::PublicRoomList, public_room_row::PublicRoomRow,
+ servers_popover::ExploreServersPopover,
};
-use crate::{session::Session, spawn, spawn_tokio};
+use crate::session::Session;
mod imp {
use std::cell::{Cell, RefCell};
@@ -34,7 +37,9 @@ mod imp {
#[template_child]
pub search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
- pub network_menu: TemplateChild<gtk::ComboBoxText>,
+ pub servers_button: TemplateChild<gtk::MenuButton>,
+ #[template_child]
+ pub servers_popover: TemplateChild<ExploreServersPopover>,
#[template_child]
pub listview: TemplateChild<gtk::ListView>,
#[template_child]
@@ -121,13 +126,17 @@ mod imp {
self.search_entry
.connect_search_changed(clone!(@weak obj => move |_| {
- let priv_ = obj.imp();
- 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);
- };
+ obj.trigger_search();
}));
+
+ self.servers_popover.connect_selected_server_changed(
+ clone!(@weak obj => move |_, server| {
+ if let Some(server) = server {
+ obj.imp().servers_button.set_label(server.name());
+ obj.trigger_search();
+ }
+ }),
+ );
}
}
@@ -150,8 +159,14 @@ impl Explore {
}
pub fn init(&self) {
- self.load_protocols();
- if let Some(public_room_list) = &*self.imp().public_room_list.borrow() {
+ let priv_ = self.imp();
+
+ priv_.servers_popover.init();
+ priv_
+ .servers_button
+ .set_label(priv_.servers_popover.selected_server().unwrap().name());
+
+ if let Some(public_room_list) = &*priv_.public_room_list.borrow() {
public_room_list.load_public_rooms(true);
}
@@ -205,38 +220,12 @@ impl Explore {
}
}
- fn set_protocols(&self, protocols: get_protocols::v3::Response) {
- for protocol in protocols
- .protocols
- .into_iter()
- .flat_map(|(_, protocol)| protocol.instances)
- {
- self.imp()
- .network_menu
- .append(Some(&protocol.instance_id), &protocol.desc);
- }
- }
-
- fn load_protocols(&self) {
- let network_menu = &self.imp().network_menu;
- let client = self.session().unwrap().client();
-
- network_menu.remove_all();
- network_menu.append(Some("matrix"), "Matrix");
- network_menu.append(Some("all"), "All rooms");
- network_menu.set_active(Some(0));
-
- let handle =
- spawn_tokio!(async move { client.send(get_protocols::v3::Request::new(), None).await });
-
- spawn!(
- glib::PRIORITY_DEFAULT_IDLE,
- clone!(@weak self as obj => async move {
- match handle.await.unwrap() {
- Ok(response) => obj.set_protocols(response),
- Err(error) => error!("Error loading supported protocols: {}", error),
- }
- })
- );
+ fn trigger_search(&self) {
+ let priv_ = self.imp();
+ if let Some(public_room_list) = &*priv_.public_room_list.borrow() {
+ let text = priv_.search_entry.text().as_str().to_string();
+ let server = priv_.servers_popover.selected_server().unwrap();
+ public_room_list.search(Some(text), server);
+ };
}
}
diff --git a/src/session/content/explore/public_room_list.rs b/src/session/content/explore/public_room_list.rs
index 8c72548df..8527cd867 100644
--- a/src/session/content/explore/public_room_list.rs
+++ b/src/session/content/explore/public_room_list.rs
@@ -9,6 +9,7 @@ use matrix_sdk::ruma::{
uint, ServerName,
};
+use super::server::Server;
use crate::{
session::{content::explore::PublicRoom, Session},
spawn, spawn_tokio,
@@ -168,24 +169,21 @@ impl PublicRoomList {
self.notify("complete");
}
- pub fn search(
- &self,
- search_term: Option<String>,
- server: Option<String>,
- network: Option<String>,
- ) {
+ pub fn search(&self, search_term: Option<String>, server: Server) {
let priv_ = self.imp();
+ let network = Some(server.network());
+ let server = server.server();
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()
+ && priv_.server.borrow().as_deref() == server
+ && priv_.network.borrow().as_deref() == network
{
return;
}
priv_.search_term.replace(search_term);
- priv_.server.replace(server);
- priv_.network.replace(network);
+ priv_.server.replace(server.map(ToOwned::to_owned));
+ priv_.network.replace(network.map(ToOwned::to_owned));
self.load_public_rooms(true);
}
diff --git a/src/session/content/explore/server.rs b/src/session/content/explore/server.rs
new file mode 100644
index 000000000..d941d2083
--- /dev/null
+++ b/src/session/content/explore/server.rs
@@ -0,0 +1,149 @@
+use gtk::{glib, prelude::*, subclass::prelude::*};
+use ruma::thirdparty::ProtocolInstance;
+
+mod imp {
+ use once_cell::{sync::Lazy, unsync::OnceCell};
+
+ use super::*;
+
+ #[derive(Debug, Default)]
+ pub struct Server {
+ /// The name of the server that is displayed in the list.
+ pub name: OnceCell<String>,
+
+ /// The ID of the network that is used during search.
+ pub network: OnceCell<String>,
+
+ /// The server name that is used during search.
+ pub server: OnceCell<String>,
+
+ /// Whether this server can be deleted from the list.
+ pub deletable: OnceCell<bool>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Server {
+ const NAME: &'static str = "Server";
+ type Type = super::Server;
+ }
+
+ impl ObjectImpl for Server {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpecString::new(
+ "name",
+ "Name",
+ "The name of the server",
+ None,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ glib::ParamSpecString::new(
+ "network",
+ "Network",
+ "The ID of the network that is used during search",
+ None,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ glib::ParamSpecString::new(
+ "server",
+ "Server",
+ "The server name that is used during search",
+ None,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ glib::ParamSpecBoolean::new(
+ "deletable",
+ "Deletable",
+ "Whether this server can be deleted from the list",
+ false,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ _obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "name" => self.name.set(value.get().unwrap()).unwrap(),
+ "network" => self.network.set(value.get().unwrap()).unwrap(),
+ "server" => {
+ if let Some(server) = value.get().unwrap() {
+ self.server.set(server).unwrap();
+ }
+ }
+ "deletable" => self.deletable.set(value.get().unwrap()).unwrap(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "name" => obj.name().to_value(),
+ "network" => obj.network().to_value(),
+ "server" => obj.server().to_value(),
+ "deletable" => obj.deletable().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+}
+
+glib::wrapper! {
+ pub struct Server(ObjectSubclass<imp::Server>);
+}
+
+impl Server {
+ pub fn with_default_server(name: &str) -> Self {
+ glib::Object::new(&[
+ ("name", &name),
+ ("network", &"matrix"),
+ ("deletable", &false),
+ ])
+ .expect("Failed to create Server")
+ }
+
+ pub fn with_third_party_protocol(protocol_id: &str, instance: &ProtocolInstance) -> Self {
+ let name = format!("{} ({protocol_id})", instance.desc);
+ glib::Object::new(&[
+ ("name", &name),
+ ("network", &instance.instance_id),
+ ("deletable", &false),
+ ])
+ .expect("Failed to create Server")
+ }
+
+ pub fn with_custom_matrix_server(server: &str) -> Self {
+ glib::Object::new(&[
+ ("name", &server),
+ ("network", &"matrix"),
+ ("server", &server),
+ ("deletable", &true),
+ ])
+ .expect("Failed to create Server")
+ }
+
+ pub fn name(&self) -> &str {
+ self.imp().name.get().unwrap()
+ }
+
+ pub fn network(&self) -> &str {
+ self.imp().network.get().unwrap()
+ }
+
+ pub fn server(&self) -> Option<&str> {
+ self.imp().server.get().map(String::as_ref)
+ }
+
+ pub fn deletable(&self) -> bool {
+ *self.imp().deletable.get().unwrap()
+ }
+}
diff --git a/src/session/content/explore/server_list.rs b/src/session/content/explore/server_list.rs
new file mode 100644
index 000000000..b0282b69d
--- /dev/null
+++ b/src/session/content/explore/server_list.rs
@@ -0,0 +1,209 @@
+use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
+use log::error;
+use ruma::api::client::thirdparty::get_protocols;
+
+use super::server::Server;
+use crate::{prelude::*, session::Session, spawn, spawn_tokio};
+
+mod imp {
+ use std::cell::RefCell;
+
+ use glib::object::WeakRef;
+ use once_cell::sync::Lazy;
+
+ use super::*;
+
+ #[derive(Debug, Default)]
+ pub struct ServerList {
+ pub session: WeakRef<Session>,
+ pub list: RefCell<Vec<Server>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for ServerList {
+ const NAME: &'static str = "ServerList";
+ type Type = super::ServerList;
+ type Interfaces = (gio::ListModel,);
+ }
+
+ impl ObjectImpl for ServerList {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpecObject::new(
+ "session",
+ "Session",
+ "The session",
+ Session::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "session" => obj.set_session(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(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+
+ impl ListModelImpl for ServerList {
+ fn item_type(&self, _list_model: &Self::Type) -> glib::Type {
+ Server::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! {
+ /// The list of servers to explore.
+ pub struct ServerList(ObjectSubclass<imp::ServerList>)
+ @implements gio::ListModel;
+}
+
+impl ServerList {
+ pub fn new(session: &Session) -> Self {
+ glib::Object::new(&[("session", session)]).expect("Failed to create ServerList")
+ }
+
+ fn set_session(&self, session: Session) {
+ let priv_ = self.imp();
+
+ priv_.session.set(Some(&session));
+
+ let user_id = session.user().unwrap().user_id();
+ priv_.list.replace(vec![Server::with_default_server(
+ user_id.server_name().as_str(),
+ )]);
+ self.items_changed(0, 0, 1);
+
+ spawn!(clone!(@weak self as obj => async move {
+ obj.load_servers().await;
+ }));
+ }
+
+ pub fn session(&self) -> Option<Session> {
+ self.imp().session.upgrade()
+ }
+
+ async fn load_servers(&self) {
+ self.load_protocols().await;
+
+ let custom_servers = self.session().unwrap().settings().explore_custom_servers();
+ self.imp().list.borrow_mut().extend(
+ custom_servers
+ .into_iter()
+ .map(|server| Server::with_custom_matrix_server(&server)),
+ );
+
+ let added = self.imp().list.borrow().len();
+ self.items_changed(1, 0, (added - 1) as u32);
+ }
+
+ async fn load_protocols(&self) {
+ let client = self.session().unwrap().client();
+
+ let handle =
+ spawn_tokio!(async move { client.send(get_protocols::v3::Request::new(), None).await });
+
+ match handle.await.unwrap() {
+ Ok(response) => self.add_protocols(response),
+ Err(error) => {
+ error!("Error loading supported protocols: {}", error);
+ }
+ }
+ }
+
+ fn add_protocols(&self, protocols: get_protocols::v3::Response) {
+ let protocols_servers =
+ protocols
+ .protocols
+ .into_iter()
+ .flat_map(|(protocol_id, protocol)| {
+ protocol.instances.into_iter().map(move |instance| {
+ Server::with_third_party_protocol(&protocol_id, &instance)
+ })
+ });
+
+ self.imp().list.borrow_mut().extend(protocols_servers)
+ }
+
+ pub fn contains_matrix_server(&self, server: &str) -> bool {
+ let list = &self.imp().list.borrow();
+ // The user's matrix server is a special case that doesn't have a "server", so
+ // use its name.
+ list[0].name() == server || list.iter().any(|s| s.server() == Some(server))
+ }
+
+ pub fn add_custom_matrix_server(&self, server_name: String) {
+ let server = Server::with_custom_matrix_server(&server_name);
+ let pos = {
+ let mut list = self.imp().list.borrow_mut();
+ let pos = list.len();
+
+ list.push(server);
+ pos
+ };
+
+ let session = self.session().unwrap();
+ let settings = session.settings();
+ let mut servers = settings.explore_custom_servers();
+ servers.push(server_name);
+ settings.set_explore_custom_servers(servers);
+
+ self.items_changed(pos as u32, 0, 1);
+ }
+
+ pub fn remove_custom_matrix_server(&self, server_name: &str) {
+ let pos = {
+ let mut list = self.imp().list.borrow_mut();
+ let pos = list
+ .iter()
+ .position(|s| s.deletable() && s.server() == Some(server_name));
+
+ if let Some(pos) = pos {
+ list.remove(pos);
+ }
+ pos
+ };
+
+ if let Some(pos) = pos {
+ let session = self.session().unwrap();
+ let settings = session.settings();
+ let servers = settings
+ .explore_custom_servers()
+ .into_iter()
+ .filter(|s| *s != server_name)
+ .collect::<Vec<_>>();
+ settings.set_explore_custom_servers(servers);
+
+ self.items_changed(pos as u32, 1, 0);
+ }
+ }
+}
diff --git a/src/session/content/explore/server_row.rs b/src/session/content/explore/server_row.rs
new file mode 100644
index 000000000..26868cc63
--- /dev/null
+++ b/src/session/content/explore/server_row.rs
@@ -0,0 +1,98 @@
+use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+use super::server::Server;
+
+mod imp {
+ use glib::subclass::InitializingObject;
+ use once_cell::{sync::Lazy, unsync::OnceCell};
+
+ use super::*;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/Fractal/content-explore-server-row.ui")]
+ pub struct ExploreServerRow {
+ /// The server displayed by this row.
+ pub server: OnceCell<Server>,
+ #[template_child]
+ pub remove_button: TemplateChild<gtk::Button>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for ExploreServerRow {
+ const NAME: &'static str = "ExploreServerRow";
+ type Type = super::ExploreServerRow;
+ type ParentType = gtk::ListBoxRow;
+
+ fn class_init(klass: &mut Self::Class) {
+ Self::bind_template(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for ExploreServerRow {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpecObject::new(
+ "server",
+ "Server",
+ "The server displayed by this row",
+ Server::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ _obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "server" => self.server.set(value.get().unwrap()).unwrap(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "server" => obj.server().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+
+ if let Some(server) = obj.server().and_then(|s| s.server()) {
+ self.remove_button.set_action_target(Some(&server));
+ self.remove_button
+ .set_action_name(Some("explore-servers-popover.remove-server"));
+ }
+ }
+ }
+
+ impl WidgetImpl for ExploreServerRow {}
+ impl ListBoxRowImpl for ExploreServerRow {}
+}
+
+glib::wrapper! {
+ pub struct ExploreServerRow(ObjectSubclass<imp::ExploreServerRow>)
+ @extends gtk::Widget, gtk::ListBoxRow, @implements gtk::Accessible;
+}
+
+impl ExploreServerRow {
+ pub fn new(server: &Server) -> Self {
+ glib::Object::new(&[("server", server)]).expect("Failed to create ExploreServerRow")
+ }
+
+ pub fn server(&self) -> Option<&Server> {
+ self.imp().server.get()
+ }
+}
diff --git a/src/session/content/explore/servers_popover.rs b/src/session/content/explore/servers_popover.rs
new file mode 100644
index 000000000..991cc2e50
--- /dev/null
+++ b/src/session/content/explore/servers_popover.rs
@@ -0,0 +1,243 @@
+use adw::subclass::prelude::*;
+use gtk::{
+ glib,
+ glib::{clone, FromVariant},
+ prelude::*,
+ CompositeTemplate,
+};
+use ruma::ServerName;
+
+use super::{server::Server, server_list::ServerList, server_row::ExploreServerRow};
+use crate::session::Session;
+
+mod imp {
+ use std::cell::RefCell;
+
+ use glib::{object::WeakRef, subclass::InitializingObject};
+ use once_cell::sync::Lazy;
+
+ use super::*;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/Fractal/content-explore-servers-popover.ui")]
+ pub struct ExploreServersPopover {
+ pub session: WeakRef<Session>,
+ pub server_list: RefCell<Option<ServerList>>,
+ #[template_child]
+ pub listbox: TemplateChild<gtk::ListBox>,
+ #[template_child]
+ pub server_entry: TemplateChild<gtk::Entry>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for ExploreServersPopover {
+ const NAME: &'static str = "ContentExploreServersPopover";
+ type Type = super::ExploreServersPopover;
+ type ParentType = gtk::Popover;
+
+ fn class_init(klass: &mut Self::Class) {
+ Self::bind_template(klass);
+
+ klass.install_action(
+ "explore-servers-popover.add-server",
+ None,
+ move |obj, _, _| {
+ obj.add_server();
+ },
+ );
+ klass.install_action(
+ "explore-servers-popover.remove-server",
+ Some("s"),
+ move |obj, _, variant| {
+ if let Some(variant) = variant.and_then(String::from_variant) {
+ obj.remove_server(&variant);
+ }
+ },
+ );
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for ExploreServersPopover {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpecObject::new(
+ "server-list",
+ "Server List",
+ "The server list",
+ ServerList::static_type(),
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpecObject::new(
+ "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() {
+ "session" => obj.set_session(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(),
+ "server-list" => obj.server_list().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+
+ self.server_entry
+ .connect_changed(clone!(@weak obj => move |_| {
+ obj.update_add_server_state()
+ }));
+ self.server_entry
+ .connect_activate(clone!(@weak obj => move |_| {
+ obj.add_server()
+ }));
+
+ obj.update_add_server_state();
+ }
+ }
+
+ impl WidgetImpl for ExploreServersPopover {}
+ impl PopoverImpl for ExploreServersPopover {}
+}
+
+glib::wrapper! {
+ pub struct ExploreServersPopover(ObjectSubclass<imp::ExploreServersPopover>)
+ @extends gtk::Widget, gtk::Popover, @implements gtk::Accessible;
+}
+
+impl ExploreServersPopover {
+ pub fn new(session: &Session) -> Self {
+ glib::Object::new(&[("session", session)]).expect("Failed to create ExploreServersPopover")
+ }
+
+ pub fn session(&self) -> Option<Session> {
+ self.imp().session.upgrade()
+ }
+
+ pub fn set_session(&self, session: Option<Session>) {
+ if session == self.session() {
+ return;
+ }
+
+ self.imp().session.set(session.as_ref());
+ self.notify("session");
+ }
+
+ pub fn init(&self) {
+ if let Some(session) = &self.session() {
+ let priv_ = self.imp();
+ let server_list = ServerList::new(session);
+
+ priv_.listbox.bind_model(Some(&server_list), |obj| {
+ ExploreServerRow::new(obj.downcast_ref::<Server>().unwrap()).upcast()
+ });
+
+ // Select the first server by default.
+ priv_
+ .listbox
+ .select_row(priv_.listbox.row_at_index(0).as_ref());
+
+ priv_.server_list.replace(Some(server_list));
+ self.notify("server-list");
+ }
+ }
+
+ pub fn server_list(&self) -> Option<ServerList> {
+ self.imp().server_list.borrow().clone()
+ }
+
+ pub fn selected_server(&self) -> Option<Server> {
+ self.imp()
+ .listbox
+ .selected_row()
+ .and_then(|row| row.downcast::<ExploreServerRow>().ok())
+ .and_then(|row| row.server().cloned())
+ }
+
+ pub fn connect_selected_server_changed<F: Fn(&Self, Option<Server>) + 'static>(
+ &self,
+ f: F,
+ ) -> glib::SignalHandlerId {
+ self.imp()
+ .listbox
+ .connect_row_selected(clone!(@weak self as obj => move |_, row| {
+ f(&obj, row.and_then(|row| row.downcast_ref::<ExploreServerRow>()).and_then(|row|
row.server().cloned()));
+ }))
+ }
+
+ fn can_add_server(&self) -> bool {
+ let server = self.imp().server_entry.text();
+ ServerName::parse(server.as_str()).is_ok()
+ // Don't allow duplicates
+ && self
+ .server_list()
+ .filter(|l| !l.contains_matrix_server(&server))
+ .is_some()
+ }
+
+ fn update_add_server_state(&self) {
+ self.action_set_enabled("explore-servers-popover.add-server", self.can_add_server())
+ }
+
+ fn add_server(&self) {
+ if !self.can_add_server() {
+ return;
+ }
+
+ if let Some(server_list) = self.server_list() {
+ let priv_ = self.imp();
+
+ let server = priv_.server_entry.text();
+ priv_.server_entry.set_text("");
+
+ server_list.add_custom_matrix_server(server.into());
+ priv_.listbox.select_row(
+ priv_
+ .listbox
+ .row_at_index(server_list.n_items() as i32 - 1)
+ .as_ref(),
+ );
+ }
+ }
+
+ fn remove_server(&self, server: &str) {
+ if let Some(server_list) = self.server_list() {
+ let priv_ = self.imp();
+
+ // If the selected server is gonna be removed, select the first one.
+ if self.selected_server().unwrap().server() == Some(server) {
+ priv_
+ .listbox
+ .select_row(priv_.listbox.row_at_index(0).as_ref());
+ }
+
+ server_list.remove_custom_matrix_server(server);
+ }
+ }
+}
diff --git a/src/session/mod.rs b/src/session/mod.rs
index 5f3020786..53265685f 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -6,6 +6,7 @@ mod media_viewer;
pub mod room;
mod room_creation;
mod room_list;
+mod settings;
mod sidebar;
mod user;
pub mod verification;
@@ -65,6 +66,7 @@ pub use self::{
avatar::Avatar,
room::{Room, SupportedEvent},
room_creation::RoomCreation,
+ settings::SessionSettings,
user::{User, UserActions, UserExt},
};
use crate::{
@@ -127,6 +129,7 @@ mod imp {
pub sync_tokio_handle: RefCell<Option<JoinHandle<()>>>,
pub offline_handler_id: RefCell<Option<SignalHandlerId>>,
pub offline: Cell<bool>,
+ pub settings: OnceCell<SessionSettings>,
}
#[glib::object_subclass]
@@ -314,6 +317,10 @@ impl Session {
glib::Object::new(&[]).expect("Failed to create Session")
}
+ pub fn session_id(&self) -> &str {
+ self.imp().info.get().unwrap().id()
+ }
+
pub fn select_room(&self, room: Option<Room>) {
self.imp()
.sidebar
@@ -502,6 +509,11 @@ impl Session {
}
};
+ priv_
+ .settings
+ .set(SessionSettings::new(session.id()))
+ .unwrap();
+
priv_.info.set(session).unwrap();
self.update_offline().await;
@@ -641,6 +653,11 @@ impl Session {
self.imp().prepared.get()
}
+ /// The current settings for this session.
+ pub fn settings(&self) -> &SessionSettings {
+ self.imp().settings.get().unwrap()
+ }
+
pub fn room_list(&self) -> &RoomList {
self.item_list().room_list()
}
@@ -862,6 +879,10 @@ impl Session {
handle.abort();
}
+ if let Some(settings) = priv_.settings.get() {
+ settings.delete_settings();
+ }
+
if let Err(error) = secret::remove_session(info).await {
error!(
"Failed to remove credentials from SecretService after logout: {}",
diff --git a/src/session/settings.rs b/src/session/settings.rs
new file mode 100644
index 000000000..cd927f2ac
--- /dev/null
+++ b/src/session/settings.rs
@@ -0,0 +1,183 @@
+use gtk::{glib, prelude::*, subclass::prelude::*};
+use serde::{Deserialize, Serialize};
+
+use crate::Application;
+
+#[derive(Serialize, Deserialize)]
+struct StoredSessionSettings {
+ /// The ID of the session these settings are for.
+ session_id: String,
+
+ /// Custom servers to explore.
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ explore_custom_servers: Vec<String>,
+}
+
+mod imp {
+ use std::cell::RefCell;
+
+ use once_cell::sync::{Lazy, OnceCell};
+
+ use super::*;
+
+ #[derive(Debug, Default)]
+ pub struct SessionSettings {
+ /// The ID of the session these settings are for.
+ pub session_id: OnceCell<String>,
+
+ /// Custom servers to explore.
+ pub explore_custom_servers: RefCell<Vec<String>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for SessionSettings {
+ const NAME: &'static str = "SessionSettings";
+ type Type = super::SessionSettings;
+ }
+
+ impl ObjectImpl for SessionSettings {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpecString::new(
+ "session-id",
+ "Session ID",
+ "The ID of the session these settings are for",
+ None,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "session-id" => obj.set_session_id(value.get().ok()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "session-id" => obj.session_id().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+}
+
+glib::wrapper! {
+ /// The settings of a `Session`.
+ pub struct SessionSettings(ObjectSubclass<imp::SessionSettings>);
+}
+
+impl SessionSettings {
+ pub fn new(session_id: &str) -> Self {
+ glib::Object::new(&[("session-id", &session_id)]).expect("Failed to create SessionSettings")
+ }
+
+ pub fn session_id(&self) -> &str {
+ self.imp().session_id.get().unwrap()
+ }
+
+ fn set_session_id(&self, session_id: Option<String>) {
+ let priv_ = self.imp();
+
+ let session_id = match session_id {
+ Some(s) => s,
+ None => return,
+ };
+
+ let app_settings = Application::default().settings();
+ let sessions =
+ serde_json::from_str::<Vec<StoredSessionSettings>>(&app_settings.string("sessions"))
+ .unwrap_or_default();
+
+ let index = sessions
+ .iter()
+ .enumerate()
+ .find_map(|(idx, settings)| (settings.session_id == session_id).then(|| idx));
+
+ priv_.session_id.set(session_id).unwrap();
+
+ if let Some(settings) = index.and_then(|idx| sessions.into_iter().nth(idx)) {
+ *priv_.explore_custom_servers.borrow_mut() = settings.explore_custom_servers;
+ } else {
+ self.store_settings();
+ }
+ }
+
+ fn store_settings(&self) {
+ let new_settings = StoredSessionSettings {
+ session_id: self.session_id().to_owned(),
+ explore_custom_servers: self.explore_custom_servers(),
+ };
+ let app_settings = Application::default().settings();
+ let mut sessions =
+ serde_json::from_str::<Vec<StoredSessionSettings>>(&app_settings.string("sessions"))
+ .unwrap_or_default();
+
+ let index = sessions.iter().enumerate().find_map(|(idx, settings)| {
+ (settings.session_id == new_settings.session_id).then(|| idx)
+ });
+ if let Some(index) = index {
+ sessions[index] = new_settings;
+ } else {
+ sessions.push(new_settings);
+ }
+
+ if let Err(error) =
+ app_settings.set_string("sessions", &serde_json::to_string(&sessions).unwrap())
+ {
+ log::error!("Error storing settings for session: {error}");
+ }
+ }
+
+ pub fn delete_settings(&self) {
+ let app_settings = Application::default().settings();
+ if let Ok(sessions) =
+ serde_json::from_str::<Vec<StoredSessionSettings>>(&app_settings.string("sessions"))
+ {
+ let session_id = self.session_id();
+ let mut found = false;
+ let sessions = sessions
+ .into_iter()
+ .filter(|settings| {
+ if settings.session_id == session_id {
+ found = true;
+ false
+ } else {
+ true
+ }
+ })
+ .collect::<Vec<_>>();
+
+ if found {
+ if let Err(error) =
+ app_settings.set_string("sessions", &serde_json::to_string(&sessions).unwrap())
+ {
+ log::error!("Error deleting settings for session: {error}");
+ }
+ }
+ }
+ }
+
+ pub fn explore_custom_servers(&self) -> Vec<String> {
+ self.imp().explore_custom_servers.borrow().clone()
+ }
+
+ pub fn set_explore_custom_servers(&self, servers: Vec<String>) {
+ if self.explore_custom_servers() == servers {
+ return;
+ }
+
+ self.imp().explore_custom_servers.replace(servers);
+ self.store_settings();
+ }
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]