Browse Source

session: Split between view and model

merge-requests/1327/merge
Kévin Commaille 3 years ago
parent
commit
e0dd94a105
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
  1. 2
      data/resources/resources.gresource.xml
  2. 2
      data/resources/ui/greeter.ui
  3. 17
      data/resources/ui/session-view.ui
  4. 8
      data/resources/ui/window.ui
  5. 8
      src/account_switcher/session_item.rs
  6. 14
      src/application.rs
  7. 24
      src/session/content/mod.rs
  8. 24
      src/session/content/room_history/message_row/mod.rs
  9. 8
      src/session/content/room_history/state_row/tombstone.rs
  10. 11
      src/session/content/room_history/verification_info_bar.rs
  11. 12
      src/session/create_dm_dialog/mod.rs
  12. 8
      src/session/join_room_dialog.rs
  13. 270
      src/session/mod.rs
  14. 11
      src/session/notifications.rs
  15. 9
      src/session/room_creation/mod.rs
  16. 105
      src/session/sidebar/mod.rs
  17. 8
      src/session/sidebar/selection.rs
  18. 133
      src/session/sidebar/sidebar_list_model.rs
  19. 339
      src/session/view.rs
  20. 92
      src/window.rs

2
data/resources/resources.gresource.xml

@ -138,7 +138,7 @@
<file compressed="true" preprocess="xml-stripblanks" alias="room-title.ui">ui/room-title.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="session-item-row.ui">ui/session-item-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="session-verification.ui">ui/session-verification.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="session.ui">ui/session.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="session-view.ui">ui/session-view.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="shortcuts.ui">ui/shortcuts.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="sidebar-account-switcher.ui">ui/sidebar-account-switcher.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="sidebar-category-row.ui">ui/sidebar-category-row.ui</file>

2
data/resources/ui/greeter.ui

@ -13,7 +13,7 @@
</property>
<child type="start">
<object class="GtkButton" id="back_button">
<property name="action-name">app.show-sessions</property>
<property name="action-name">app.show-session</property>
<property name="visible" bind-source="back_button" bind-property="sensitive" bind-flags="sync-create"/>
<property name="icon-name">go-previous-symbolic</property>
</object>

17
data/resources/ui/session.ui → data/resources/ui/session-view.ui

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="Session" parent="AdwBin">
<template class="SessionView" parent="AdwBin">
<property name="child">
<object class="GtkStack" id="stack">
<property name="visible-child">overlay</property>
@ -18,8 +18,16 @@
<child>
<object class="Sidebar" id="sidebar">
<property name="compact" bind-source="leaflet" bind-property="folded" bind-flags="sync-create"/>
<property name="user" bind-source="Session" bind-property="user" bind-flags="sync-create"/>
<property name="item-list" bind-source="Session" bind-property="item-list" bind-flags="sync-create"/>
<binding name="user">
<lookup name="user">
<lookup name="session">SessionView</lookup>
</lookup>
</binding>
<binding name="list-model">
<lookup name="sidebar-list-model">
<lookup name="session">SessionView</lookup>
</lookup>
</binding>
</object>
</child>
<child>
@ -33,8 +41,7 @@
<child>
<object class="Content" id="content">
<property name="compact" bind-source="leaflet" bind-property="folded" bind-flags="sync-create"/>
<property name="item" bind-source="sidebar" bind-property="selected-item" bind-flags="sync-create | bidirectional"/>
<property name="session">Session</property>
<property name="session" bind-source="SessionView" bind-property="session" bind-flags="sync-create"/>
</object>
</child>
</object>

8
data/resources/ui/window.ui

@ -46,8 +46,12 @@
<object class="Login" id="login"/>
</child>
<child>
<object class="GtkStack" id="sessions">
<property name="transition-type">crossfade</property>
<object class="SessionView" id="session">
<binding name="session">
<lookup name="selected-item">
<lookup name="session-selection">Window</lookup>
</lookup>
</binding>
</object>
</child>
<child>

8
src/account_switcher/session_item.rs

@ -123,9 +123,11 @@ impl SessionItemRow {
self.activate_action("account-switcher.close", None)
.unwrap();
session
.activate_action("session.open-account-settings", None)
.unwrap();
self.activate_action(
"win.open-account-settings",
Some(&session.session_id().to_variant()),
)
.unwrap();
}
/// The session this item represents.

14
src/application.rs

@ -142,20 +142,20 @@ impl Application {
.build(),
]);
let show_sessions_action = gio::SimpleAction::new("show-sessions", None);
show_sessions_action.connect_activate(clone!(@weak self as app => move |_, _| {
app.main_window().switch_to_sessions_page();
let show_session_action = gio::SimpleAction::new("show-session", None);
show_session_action.connect_activate(clone!(@weak self as app => move |_, _| {
app.main_window().switch_to_session_page();
}));
self.add_action(&show_sessions_action);
self.add_action(&show_session_action);
let win = self.main_window();
let session_list = win.session_list();
session_list.connect_is_empty_notify(
clone!(@weak show_sessions_action => move |session_list| {
show_sessions_action.set_enabled(!session_list.is_empty());
clone!(@weak show_session_action => move |session_list| {
show_session_action.set_enabled(!session_list.is_empty());
}),
);
show_sessions_action.set_enabled(!session_list.is_empty());
show_session_action.set_enabled(!session_list.is_empty());
}
/// Sets up keyboard shortcuts for application and window actions.

24
src/session/content/mod.rs

@ -31,6 +31,7 @@ mod imp {
pub struct Content {
pub compact: Cell<bool>,
pub session: WeakRef<Session>,
pub item_binding: RefCell<Option<glib::Binding>>,
pub item: RefCell<Option<glib::Object>>,
pub signal_handler: RefCell<Option<SignalHandlerId>>,
#[template_child]
@ -115,6 +116,10 @@ mod imp {
imp.identity_verification_widget.set_request(None);
}
}));
if let Some(binding) = self.item_binding.take() {
binding.unbind()
}
}
}
@ -156,7 +161,24 @@ impl Content {
return;
}
self.imp().session.set(session.as_ref());
let imp = self.imp();
if let Some(binding) = imp.item_binding.take() {
binding.unbind();
}
if let Some(session) = &session {
let item_binding = session
.sidebar_list_model()
.selection_model()
.bind_property("selected-item", self, "item")
.sync_create()
.build();
imp.item_binding.replace(Some(item_binding));
}
imp.session.set(session.as_ref());
self.notify("session");
}

24
src/session/content/room_history/message_row/mod.rs

@ -19,7 +19,7 @@ use matrix_sdk::ruma::events::room::message::MessageType;
pub use self::content::ContentFormat;
use self::{content::MessageContent, media::MessageMedia, reaction_list::MessageReactionList};
use super::ReadReceiptsList;
use crate::{components::Avatar, prelude::*, session::room::Event};
use crate::{components::Avatar, prelude::*, session::room::Event, window::Window};
mod imp {
use std::cell::RefCell;
@ -228,15 +228,23 @@ impl MessageRow {
self.imp().content.texture()
}
/// Open the media viewer with the media content of this row.
fn show_media(&self) {
let imp = self.imp();
if let Some(event) = imp.event.borrow().as_ref() {
if let Some(message) = event.message() {
if matches!(message, MessageType::Image(_) | MessageType::Video(_)) {
let media_widget = imp.content.child().and_downcast::<MessageMedia>().unwrap();
event.room().session().show_media(event, &media_widget);
}
}
let Some(window) = self.root().and_downcast::<Window>() else {
return;
};
let borrowed_event = imp.event.borrow();
let Some(event) = borrowed_event.as_ref() else {
return;
};
let Some(message) = event.message() else {
return;
};
if matches!(message, MessageType::Image(_) | MessageType::Video(_)) {
let media_widget = imp.content.child().and_downcast::<MessageMedia>().unwrap();
window.session_view().show_media(event, &media_widget);
}
}
}

8
src/session/content/room_history/state_row/tombstone.rs

@ -2,7 +2,7 @@ use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{glib, glib::clone, CompositeTemplate};
use crate::{session::Room, spawn, toast, utils::BoundObjectWeakRef};
use crate::{session::Room, spawn, toast, utils::BoundObjectWeakRef, window::Window};
mod imp {
use glib::subclass::InitializingObject;
@ -138,7 +138,11 @@ impl StateTombstone {
// Join or view the room with the given identifier.
if let Some(successor_room) = room_list.joined_room(successor.into()) {
session.select_room(Some(successor_room));
let Some(window) = self.root().and_downcast::<Window>() else {
return;
};
window.session_view().select_room(Some(successor_room));
} else {
let successor = successor.to_owned();

11
src/session/content/room_history/verification_info_bar.rs

@ -8,6 +8,7 @@ use crate::{
user::UserExt,
verification::{IdentityVerification, VerificationState},
},
window::Window,
};
mod imp {
@ -45,10 +46,14 @@ mod imp {
klass.set_accessible_role(gtk::AccessibleRole::Group);
klass.install_action("verification.accept", None, move |widget, _, _| {
let request = widget.request().unwrap();
klass.install_action("verification.accept", None, move |obj, _, _| {
let Some(window) = obj.root().and_downcast::<Window>() else {
return;
};
let request = obj.request().unwrap();
request.accept();
request.session().select_item(Some(request.upcast()));
window.session_view().select_item(Some(request.upcast()));
});
klass.install_action("verification.decline", None, move |widget, _, _| {

12
src/session/create_dm_dialog/mod.rs

@ -10,11 +10,7 @@ use self::{
dm_user_list::{DmUserList, DmUserListState},
dm_user_row::DmUserRow,
};
use crate::{
gettext,
session::{user::UserExt, Session},
spawn,
};
use crate::{gettext, session::Session, spawn, window::Window};
mod imp {
use glib::{object::WeakRef, subclass::InitializingObject};
@ -197,7 +193,11 @@ impl CreateDmDialog {
async fn start_chat(&self, user: &DmUser) {
match user.start_chat().await {
Ok(room) => {
user.session().select_room(Some(room));
let Some(window) = self.transient_for().and_downcast::<Window>() else {
return;
};
window.session_view().select_room(Some(room));
self.close();
}
Err(_) => {

8
src/session/join_room_dialog.rs

@ -6,7 +6,7 @@ use ruma::{
RoomOrAliasId,
};
use crate::{session::Session, spawn, toast};
use crate::{session::Session, spawn, toast, window::Window};
mod imp {
use glib::{object::WeakRef, subclass::InitializingObject};
@ -159,7 +159,11 @@ impl JoinRoomDialog {
// Join or view the room with the given identifier.
if let Some(room) = room_list.joined_room((&*room_id).into()) {
session.select_room(Some(room));
let Some(window) = self.root().and_downcast::<Window>() else {
return;
};
window.session_view().select_room(Some(room));
} else {
spawn!(clone!(@weak self as obj, @weak room_list => async move {
if let Err(error) = room_list.join_by_id_or_alias(room_id, via).await {

270
src/session/mod.rs

@ -13,6 +13,7 @@ mod settings;
mod sidebar;
mod user;
pub mod verification;
mod view;
use std::{collections::HashSet, time::Duration};
@ -20,11 +21,10 @@ use adw::{prelude::*, subclass::prelude::*};
use futures::StreamExt;
use gettextrs::gettext;
use gtk::{
self, gdk, gio, glib,
self, gio, glib,
glib::{clone, signal::SignalHandlerId},
CompositeTemplate,
};
use log::{debug, error, warn};
use log::{debug, error};
use matrix_sdk::{
config::SyncSettings,
room::Room as MatrixRoom,
@ -39,7 +39,6 @@ use matrix_sdk::{
direct::DirectEventContent, room::encryption::SyncRoomEncryptionEvent,
GlobalAccountDataEvent,
},
RoomId,
},
sync::SyncResponse,
Client,
@ -47,12 +46,8 @@ use matrix_sdk::{
use tokio::task::JoinHandle;
use url::Url;
use self::{
account_settings::AccountSettings, content::Content, join_room_dialog::JoinRoomDialog,
media_viewer::MediaViewer, notifications::Notifications, room_list::RoomList, sidebar::Sidebar,
verification::VerificationList,
};
pub use self::{
account_settings::AccountSettings,
avatar::{AvatarData, AvatarImage, AvatarUriSource},
content::verification::SessionVerification,
create_dm_dialog::CreateDmDialog,
@ -60,16 +55,20 @@ pub use self::{
room_creation::RoomCreation,
settings::SessionSettings,
user::{User, UserActions, UserExt},
view::SessionView,
};
use self::{
media_viewer::MediaViewer, notifications::Notifications, room_list::RoomList,
verification::VerificationList,
};
use crate::{
secret::StoredSession,
session::sidebar::ItemList,
session::sidebar::{ItemList, SidebarListModel},
spawn, spawn_tokio,
utils::{
check_if_reachable,
matrix::{self, ClientSetupError},
},
Window,
};
/// The state of the session.
@ -91,28 +90,14 @@ struct BoxedStoredSession(StoredSession);
mod imp {
use std::cell::{Cell, RefCell};
use glib::subclass::InitializingObject;
use once_cell::{sync::Lazy, unsync::OnceCell};
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Fractal/session.ui")]
#[derive(Debug, Default)]
pub struct Session {
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub overlay: TemplateChild<gtk::Overlay>,
#[template_child]
pub leaflet: TemplateChild<adw::Leaflet>,
#[template_child]
pub sidebar: TemplateChild<Sidebar>,
#[template_child]
pub content: TemplateChild<Content>,
#[template_child]
pub media_viewer: TemplateChild<MediaViewer>,
pub client: OnceCell<Client>,
pub item_list: OnceCell<ItemList>,
pub sidebar_list_model: OnceCell<SidebarListModel>,
pub user: OnceCell<User>,
pub state: Cell<SessionState>,
pub info: OnceCell<StoredSession>,
@ -120,7 +105,6 @@ mod imp {
pub offline_handler_id: RefCell<Option<SignalHandlerId>>,
pub offline: Cell<bool>,
pub settings: OnceCell<SessionSettings>,
pub window_active_handler_id: RefCell<Option<SignalHandlerId>>,
pub notifications: Notifications,
}
@ -128,77 +112,6 @@ mod imp {
impl ObjectSubclass for Session {
const NAME: &'static str = "Session";
type Type = super::Session;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
klass.install_action("session.close-room", None, move |session, _, _| {
session.select_room(None);
});
klass.install_action(
"session.show-room",
Some("s"),
move |session, _, parameter| {
if let Ok(room_id) =
<&RoomId>::try_from(&*parameter.unwrap().get::<String>().unwrap())
{
session.select_room_by_id(room_id);
} else {
error!("Can't show room because the provided id is invalid");
}
},
);
klass.install_action("session.show-content", None, move |session, _, _| {
session.show_content();
});
klass.install_action("session.room-creation", None, move |session, _, _| {
session.show_room_creation_dialog();
});
klass.install_action("session.show-join-room", None, move |widget, _, _| {
spawn!(clone!(@weak widget => async move {
widget.show_join_room_dialog().await;
}));
});
klass.install_action("session.create-dm", None, move |session, _, _| {
session.show_create_dm_dialog();
});
klass.add_binding_action(
gdk::Key::Escape,
gdk::ModifierType::empty(),
"session.close-room",
None,
);
klass.install_action("session.toggle-room-search", None, move |session, _, _| {
session.toggle_room_search();
});
klass.add_binding_action(
gdk::Key::k,
gdk::ModifierType::CONTROL_MASK,
"session.toggle-room-search",
None,
);
klass.install_action(
"session.open-account-settings",
None,
move |widget, _, _| {
widget.open_account_settings();
},
);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for Session {
@ -212,7 +125,7 @@ mod imp {
glib::ParamSpecString::builder("session-id")
.read_only()
.build(),
glib::ParamSpecObject::builder::<ItemList>("item-list")
glib::ParamSpecObject::builder::<SidebarListModel>("sidebar-list-model")
.read_only()
.build(),
glib::ParamSpecObject::builder::<User>("user")
@ -245,7 +158,7 @@ mod imp {
match pspec.name() {
"session-id" => obj.session_id().to_value(),
"item-list" => obj.item_list().to_value(),
"sidebar-list-model" => obj.sidebar_list_model().to_value(),
"user" => obj.user().to_value(),
"offline" => obj.is_offline().to_value(),
"state" => obj.state().to_value(),
@ -263,17 +176,6 @@ mod imp {
self.notifications.set_session(Some(&obj));
self.sidebar.connect_notify_local(
Some("selected-item"),
clone!(@weak self as imp => move |_, _| {
if imp.sidebar.selected_item().is_none() {
imp.leaflet.navigate(adw::NavigationDirection::Back);
} else {
imp.leaflet.navigate(adw::NavigationDirection::Forward);
}
}),
);
let monitor = gio::NetworkMonitor::default();
let handler_id = monitor.connect_network_changed(clone!(@weak obj => move |_, _| {
spawn!(clone!(@weak obj => async move {
@ -282,35 +184,9 @@ mod imp {
}));
self.offline_handler_id.replace(Some(handler_id));
self.content.connect_notify_local(
Some("item"),
clone!(@weak obj => move |_, _| {
// When switching to a room, withdraw its notifications.
obj.notifications().withdraw_all_for_selected_room();
}),
);
obj.connect_parent_notify(|obj| {
if let Some(window) = obj.root().and_then(|root| root.downcast::<Window>().ok()) {
let handler_id =
window.connect_is_active_notify(clone!(@weak obj => move |window| {
// When the window becomes active, withdraw the notifications
// of the room that is displayed.
if window.is_active()
&& window.current_session_id().as_deref() == Some(obj.session_id())
{
obj.notifications().withdraw_all_for_selected_room();
}
}));
obj.imp().window_active_handler_id.replace(Some(handler_id));
}
});
}
fn dispose(&self) {
let obj = self.obj();
// Needs to be disconnected or else it may restart the sync
if let Some(handler_id) = self.offline_handler_id.take() {
gio::NetworkMonitor::default().disconnect(handler_id);
@ -319,21 +195,13 @@ mod imp {
if let Some(handle) = self.sync_tokio_handle.take() {
handle.abort();
}
if let Some(handler_id) = self.window_active_handler_id.take() {
if let Some(window) = obj.root().and_then(|root| root.downcast::<Window>().ok()) {
window.disconnect(handler_id);
}
}
}
}
impl WidgetImpl for Session {}
impl BinImpl for Session {}
}
glib::wrapper! {
pub struct Session(ObjectSubclass<imp::Session>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
/// A Matrix user session.
pub struct Session(ObjectSubclass<imp::Session>);
}
impl Session {
@ -394,37 +262,6 @@ impl Session {
self.notify("state");
}
/// The currently selected room, if any.
pub fn selected_room(&self) -> Option<Room> {
self.imp()
.content
.item()
.and_then(|item| item.downcast().ok())
}
pub fn select_room(&self, room: Option<Room>) {
self.imp()
.sidebar
.set_selected_item(room.map(|item| item.upcast()));
}
pub fn select_item(&self, item: Option<glib::Object>) {
self.imp().sidebar.set_selected_item(item);
}
pub fn select_room_by_id(&self, room_id: &RoomId) {
if let Some(room) = self.room_list().get(room_id) {
self.select_room(Some(room));
} else {
warn!("A room with id {room_id} couldn't be found");
}
}
fn toggle_room_search(&self) {
let room_search = self.imp().sidebar.room_search_bar();
room_search.set_search_mode(!room_search.is_search_mode());
}
pub async fn prepare(&self) {
self.update_user_profile();
self.update_offline().await;
@ -525,18 +362,19 @@ impl Session {
}
pub fn room_list(&self) -> &RoomList {
self.item_list().room_list()
self.sidebar_list_model().item_list().room_list()
}
pub fn verification_list(&self) -> &VerificationList {
self.item_list().verification_list()
self.sidebar_list_model().item_list().verification_list()
}
/// The list of items in the sidebar.
pub fn item_list(&self) -> &ItemList {
self.imp()
.item_list
.get_or_init(|| ItemList::new(&RoomList::new(self), &VerificationList::new(self)))
/// The list model of the sidebar.
pub fn sidebar_list_model(&self) -> &SidebarListModel {
self.imp().sidebar_list_model.get_or_init(|| {
let item_list = ItemList::new(&RoomList::new(self), &VerificationList::new(self));
SidebarListModel::new(&item_list)
})
}
/// The user of this session.
@ -656,41 +494,9 @@ impl Session {
}
}
/// Returns the parent GtkWindow containing this widget.
fn parent_window(&self) -> Option<Window> {
self.root()?.downcast().ok()
}
fn open_account_settings(&self) {
let window = AccountSettings::new(self.parent_window().as_ref(), self);
window.present();
}
fn show_room_creation_dialog(&self) {
let window = RoomCreation::new(self.parent_window().as_ref(), self);
window.present();
}
fn show_create_dm_dialog(&self) {
let window = CreateDmDialog::new(self.parent_window().as_ref(), self);
window.present();
}
async fn show_join_room_dialog(&self) {
let dialog = JoinRoomDialog::new(self.parent_window().as_ref(), self);
dialog.present();
}
pub async fn logout(&self) -> Result<(), String> {
let stack = &self.imp().stack;
debug!("The session is about to be logged out");
// First stop the verification in progress
if let Some(session_verification) = stack.child_by_name("session-verification") {
stack.remove(&session_verification);
}
let client = self.client();
let handle = spawn_tokio!(async move {
let request = logout::v3::Request::new();
@ -726,10 +532,6 @@ impl Session {
);
}
pub fn handle_paste_action(&self) {
self.imp().content.handle_paste_action();
}
async fn cleanup_session(&self) {
let imp = self.imp();
@ -750,30 +552,6 @@ impl Session {
debug!("The logged out session was cleaned up");
}
/// Show the content of the session
pub fn show_content(&self) {
let imp = self.imp();
imp.stack.set_visible_child(&*imp.overlay);
if let Some(window) = self.parent_window() {
window.switch_to_sessions_page();
}
}
/// Show a media event.
pub fn show_media(&self, event: &Event, source_widget: &impl IsA<gtk::Widget>) {
let Some(message) = event.message() else {
error!("Trying to open the media viewer with an event that is not a message");
return;
};
let imp = self.imp();
imp.media_viewer
.set_message(&event.room(), event.event_id().unwrap(), message);
imp.media_viewer.reveal(source_widget);
}
fn setup_direct_room_handler(&self) {
spawn!(
glib::PRIORITY_DEFAULT_IDLE,

11
src/session/notifications.rs

@ -5,7 +5,7 @@ use ruma::{
RoomId,
};
use super::Session;
use super::{Room, Session};
use crate::{
application::AppShowRoomPayload, prelude::*, utils::matrix::get_event_body, Application,
};
@ -171,20 +171,15 @@ impl Notifications {
.push(event_id.to_owned());
}
/// Ask the system to remove the known notifications for the currently
/// selected room.
/// Ask the system to remove the known notifications for the given room.
///
/// Only the notifications that were shown since the application's startup
/// are known, older ones might still be present.
pub fn withdraw_all_for_selected_room(&self) {
pub fn withdraw_all_for_room(&self, room: &Room) {
let Some(session) = self.session() else {
return;
};
let Some(room) = session.selected_room() else {
return;
};
let room_id = room.room_id();
if let Some(notifications) = self.imp().list.borrow_mut().remove(room_id) {
let app = Application::default();

9
src/session/room_creation/mod.rs

@ -17,7 +17,9 @@ use ruma::events::{room::encryption::RoomEncryptionEventContent, InitialStateEve
use crate::{
components::SpinnerButton,
session::{user::UserExt, Session},
spawn, spawn_tokio, UserFacingError,
spawn, spawn_tokio,
window::Window,
UserFacingError,
};
// MAX length of room addresses
@ -202,8 +204,11 @@ impl RoomCreation {
match handle.await.unwrap() {
Ok(matrix_room) => {
if let Some(session) = obj.session() {
let Some(window) = obj.transient_for().and_downcast::<Window>() else {
return;
};
let room = session.room_list().get_wait(matrix_room.room_id()).await;
session.select_room(room);
window.session_view().select_room(room);
}
obj.close();
},

105
src/session/sidebar/mod.rs

@ -7,6 +7,7 @@ mod room_row;
mod row;
mod selection;
mod sidebar_item;
mod sidebar_list_model;
mod verification_row;
use adw::{prelude::*, subclass::prelude::*};
@ -14,7 +15,7 @@ use gtk::{gio, glib, glib::clone, CompositeTemplate};
use log::error;
use self::{
category::CategoryRow, entry_row::EntryRow, room_row::RoomRow, row::Row, selection::Selection,
category::CategoryRow, entry_row::EntryRow, room_row::RoomRow, row::Row,
verification_row::VerificationRow,
};
pub use self::{
@ -22,7 +23,9 @@ pub use self::{
entry::Entry,
entry_type::EntryType,
item_list::ItemList,
selection::Selection,
sidebar_item::{SidebarItem, SidebarItemExt, SidebarItemImpl},
sidebar_list_model::SidebarListModel,
};
use crate::{
components::Avatar,
@ -47,7 +50,6 @@ mod imp {
#[template(resource = "/org/gnome/Fractal/sidebar.ui")]
pub struct Sidebar {
pub compact: Cell<bool>,
pub selected_item: RefCell<Option<glib::Object>>,
#[template_child]
pub headerbar: TemplateChild<adw::HeaderBar>,
#[template_child]
@ -70,7 +72,9 @@ mod imp {
pub drop_source_type: Cell<Option<RoomType>>,
/// The type of the drop target that is currently hovered.
pub drop_active_target_type: Cell<Option<RoomType>>,
pub drop_binding: RefCell<Option<glib::Binding>>,
/// The list model of this sidebar.
pub list_model: glib::WeakRef<SidebarListModel>,
pub bindings: RefCell<Vec<glib::Binding>>,
pub offline_handler_id: RefCell<Option<SignalHandlerId>>,
}
@ -103,10 +107,7 @@ mod imp {
glib::ParamSpecBoolean::builder("compact")
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<ItemList>("item-list")
.write_only()
.build(),
glib::ParamSpecObject::builder::<glib::Object>("selected-item")
glib::ParamSpecObject::builder::<SidebarListModel>("list-model")
.explicit_notify()
.build(),
glib::ParamSpecEnum::builder::<CategoryType>("drop-source-type")
@ -127,8 +128,7 @@ mod imp {
match pspec.name() {
"compact" => obj.set_compact(value.get().unwrap()),
"user" => obj.set_user(value.get().unwrap()),
"item-list" => obj.set_item_list(value.get().unwrap()),
"selected-item" => obj.set_selected_item(value.get().unwrap()),
"list-model" => obj.set_list_model(value.get().unwrap()),
_ => unimplemented!(),
}
}
@ -139,7 +139,7 @@ mod imp {
match pspec.name() {
"compact" => obj.compact().to_value(),
"user" => obj.user().to_value(),
"selected-item" => obj.selected_item().to_value(),
"list-model" => obj.list_model().to_value(),
"drop-source-type" => obj
.drop_source_type()
.map(CategoryType::from)
@ -262,70 +262,51 @@ impl Sidebar {
self.imp().compact.set(compact)
}
/// The selected item in this sidebar.
pub fn selected_item(&self) -> Option<glib::Object> {
self.imp().selected_item.borrow().clone()
}
pub fn room_search_bar(&self) -> gtk::SearchBar {
self.imp().room_search.clone()
}
/// Set the list of items in the sidebar.
pub fn set_item_list(&self, item_list: Option<ItemList>) {
/// The list model of this sidebar.
pub fn list_model(&self) -> Option<SidebarListModel> {
self.imp().list_model.upgrade()
}
/// Set the list model of the sidebar.
pub fn set_list_model(&self, list_model: Option<SidebarListModel>) {
if self.list_model() == list_model {
return;
}
let imp = self.imp();
if let Some(binding) = imp.drop_binding.take() {
for binding in imp.bindings.take() {
binding.unbind();
}
let item_list = match item_list {
Some(item_list) => item_list,
None => {
imp.listview.set_model(gtk::SelectionModel::NONE);
return;
}
};
imp.drop_binding.replace(Some(
self.bind_property("drop-source-type", &item_list, "show-all-for-category")
.flags(glib::BindingFlags::SYNC_CREATE)
if let Some(list_model) = &list_model {
let bindings = vec![
self.bind_property(
"drop-source-type",
list_model.item_list(),
"show-all-for-category",
)
.sync_create()
.build(),
));
let tree_model = gtk::TreeListModel::new(item_list, false, true, |item| {
item.clone().downcast::<gio::ListModel>().ok()
});
let room_expression =
gtk::TreeListRow::this_expression("item").chain_property::<Room>("display-name");
let filter = gtk::StringFilter::builder()
.match_mode(gtk::StringFilterMatchMode::Substring)
.expression(&room_expression)
.ignore_case(true)
.build();
imp.room_search_entry
.bind_property("text", &filter, "search")
.flags(glib::BindingFlags::SYNC_CREATE)
.build();
let filter_model = gtk::FilterListModel::new(Some(tree_model), Some(filter));
let selection = Selection::new(Some(&filter_model));
self.bind_property("selected-item", &selection, "selected-item")
.flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
.build();
imp.listview.set_model(Some(&selection));
}
/// Set the selected item in this sidebar.
pub fn set_selected_item(&self, selected_item: Option<glib::Object>) {
if self.selected_item() == selected_item {
return;
list_model
.string_filter()
.bind_property("search", &*imp.room_search_entry, "text")
.sync_create()
.bidirectional()
.build(),
];
imp.bindings.replace(bindings);
}
self.imp().selected_item.replace(selected_item);
self.notify("selected-item");
imp.listview
.set_model(list_model.as_ref().map(|m| m.selection_model()));
imp.list_model.set(list_model.as_ref());
self.notify("list-model");
}
/// The logged-in user.

8
src/session/sidebar/selection.rs

@ -228,7 +228,7 @@ impl Selection {
}
/// Set the selected item.
fn set_selected_item(&self, item: Option<glib::Object>) {
pub fn set_selected_item(&self, item: Option<glib::Object>) {
let imp = self.imp();
let selected_item = self.selected_item();
@ -314,3 +314,9 @@ impl Selection {
self.items_changed(position, removed, added);
}
}
impl Default for Selection {
fn default() -> Self {
Self::new(gio::ListModel::NONE)
}
}

133
src/session/sidebar/sidebar_list_model.rs

@ -0,0 +1,133 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use super::{item_list::ItemList, selection::Selection};
use crate::session::Room;
mod imp {
use once_cell::{sync::Lazy, unsync::OnceCell};
use super::*;
#[derive(Debug, Default)]
pub struct SidebarListModel {
/// The list of items in the sidebar.
pub item_list: OnceCell<ItemList>,
/// The tree list model.
pub tree_model: OnceCell<gtk::TreeListModel>,
/// The string filter.
pub string_filter: gtk::StringFilter,
/// The selection model.
pub selection_model: Selection,
}
#[glib::object_subclass]
impl ObjectSubclass for SidebarListModel {
const NAME: &'static str = "SidebarListModel";
type Type = super::SidebarListModel;
}
impl ObjectImpl for SidebarListModel {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<ItemList>("item-list")
.construct_only()
.build(),
glib::ParamSpecObject::builder::<gtk::TreeListModel>("tree-model")
.read_only()
.build(),
glib::ParamSpecObject::builder::<gtk::StringFilter>("string-filter")
.read_only()
.build(),
glib::ParamSpecObject::builder::<Selection>("selection-model")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"item-list" => obj.set_item_list(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"item-list" => obj.item_list().to_value(),
"tree-model" => obj.tree_model().to_value(),
"string-filter" => obj.string_filter().to_value(),
"selection-model" => obj.selection_model().to_value(),
_ => unimplemented!(),
}
}
}
}
glib::wrapper! {
/// A wrapper for the sidebar list model of a `Session`.
///
/// It allows to keep the state for selection and filtering.
pub struct SidebarListModel(ObjectSubclass<imp::SidebarListModel>);
}
impl SidebarListModel {
/// Create a new `SidebarListModel`.
pub fn new(item_list: &ItemList) -> Self {
glib::Object::builder()
.property("item-list", item_list)
.build()
}
/// The list of items in the sidebar.
pub fn item_list(&self) -> &ItemList {
self.imp().item_list.get().unwrap()
}
/// Set the list of items in the sidebar.
fn set_item_list(&self, item_list: ItemList) {
let imp = self.imp();
imp.item_list.set(item_list.clone()).unwrap();
let tree_model =
gtk::TreeListModel::new(item_list, false, true, |item| item.clone().downcast().ok());
imp.tree_model.set(tree_model.clone()).unwrap();
let room_expression =
gtk::TreeListRow::this_expression("item").chain_property::<Room>("display-name");
imp.string_filter
.set_match_mode(gtk::StringFilterMatchMode::Substring);
imp.string_filter.set_expression(Some(&room_expression));
imp.string_filter.set_ignore_case(true);
// Default to an empty string to be able to bind to GtkEditable::text.
imp.string_filter.set_search(Some(""));
let filter_model =
gtk::FilterListModel::new(Some(tree_model), Some(imp.string_filter.clone()));
imp.selection_model.set_model(Some(&filter_model));
}
/// The tree list model.
pub fn tree_model(&self) -> &gtk::TreeListModel {
self.imp().tree_model.get().unwrap()
}
/// The string filter.
pub fn string_filter(&self) -> &gtk::StringFilter {
&self.imp().string_filter
}
/// The selection model.
pub fn selection_model(&self) -> &Selection {
&self.imp().selection_model
}
}

339
src/session/view.rs

@ -0,0 +1,339 @@
use adw::{prelude::*, subclass::prelude::*};
use gtk::{
self, gdk, glib,
glib::{clone, signal::SignalHandlerId},
CompositeTemplate,
};
use log::{error, warn};
use ruma::RoomId;
use super::{
content::Content,
join_room_dialog::JoinRoomDialog,
media_viewer::MediaViewer,
sidebar::{Selection, Sidebar, SidebarListModel},
CreateDmDialog, Event, Room, RoomCreation, Session,
};
use crate::{spawn, toast, Window};
mod imp {
use std::cell::RefCell;
use glib::subclass::InitializingObject;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Fractal/session-view.ui")]
pub struct SessionView {
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub overlay: TemplateChild<gtk::Overlay>,
#[template_child]
pub leaflet: TemplateChild<adw::Leaflet>,
#[template_child]
pub sidebar: TemplateChild<Sidebar>,
#[template_child]
pub content: TemplateChild<Content>,
#[template_child]
pub media_viewer: TemplateChild<MediaViewer>,
pub session: glib::WeakRef<Session>,
pub window_active_handler_id: RefCell<Option<SignalHandlerId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for SessionView {
const NAME: &'static str = "SessionView";
type Type = super::SessionView;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
klass.install_action("session.close-room", None, move |obj, _, _| {
obj.select_room(None);
});
klass.install_action("session.show-room", Some("s"), move |obj, _, parameter| {
if let Ok(room_id) =
<&RoomId>::try_from(&*parameter.unwrap().get::<String>().unwrap())
{
obj.select_room_by_id(room_id);
} else {
error!("Cannot show room with invalid ID");
}
});
klass.install_action("session.logout", None, move |obj, _, _| {
if let Some(session) = obj.session() {
spawn!(clone!(@weak obj, @weak session => async move {
if let Err(error) = session.logout().await {
toast!(obj, error);
}
}));
}
});
klass.install_action("session.show-content", None, move |obj, _, _| {
obj.show_content();
});
klass.install_action("session.room-creation", None, move |obj, _, _| {
obj.show_room_creation_dialog();
});
klass.install_action("session.show-join-room", None, move |obj, _, _| {
spawn!(clone!(@weak obj => async move {
obj.show_join_room_dialog().await;
}));
});
klass.install_action("session.create-dm", None, move |obj, _, _| {
obj.show_create_dm_dialog();
});
klass.add_binding_action(
gdk::Key::Escape,
gdk::ModifierType::empty(),
"session.close-room",
None,
);
klass.install_action("session.toggle-room-search", None, move |obj, _, _| {
obj.toggle_room_search();
});
klass.add_binding_action(
gdk::Key::k,
gdk::ModifierType::CONTROL_MASK,
"session.toggle-room-search",
None,
);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for SessionView {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<Session>("session")
.explicit_notify()
.build()]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"session" => obj.set_session(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"session" => obj.session().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
self.sidebar.property_expression("list-model").chain_property::<SidebarListModel>("selection-model").chain_property::<Selection>("selected-item").watch(glib::Object::NONE,
clone!(@weak self as imp => move || {
if imp.sidebar.list_model().and_then(|m| m.selection_model().selected_item()).is_none() {
imp.leaflet.navigate(adw::NavigationDirection::Back);
} else {
imp.leaflet.navigate(adw::NavigationDirection::Forward);
}
}),
);
self.content.connect_notify_local(
Some("item"),
clone!(@weak obj => move |_, _| {
let Some(session) = obj.session() else {
return;
};
let Some(room) = obj.selected_room() else {
return;
};
// When switching to a room, withdraw its notifications.
session.notifications().withdraw_all_for_room(&room);
}),
);
obj.connect_root_notify(|obj| {
let Some(window) = obj.parent_window() else {
return;
};
let handler_id =
window.connect_is_active_notify(clone!(@weak obj => move |window| {
if !window.is_active() {
return;
}
let Some(session) = obj.session() else {
return;
};
let Some(room) = obj.selected_room() else {
return;
};
// When the window becomes active, withdraw the notifications
// of the room that is displayed.
session.notifications().withdraw_all_for_room(&room);
}));
obj.imp().window_active_handler_id.replace(Some(handler_id));
});
}
fn dispose(&self) {
if let Some(handler_id) = self.window_active_handler_id.take() {
if let Some(window) = self.obj().parent_window() {
window.disconnect(handler_id);
}
}
}
}
impl WidgetImpl for SessionView {}
impl BinImpl for SessionView {}
}
glib::wrapper! {
/// A view for a Matrix user session.
pub struct SessionView(ObjectSubclass<imp::SessionView>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl SessionView {
/// Create a new session.
pub async fn new() -> Self {
glib::Object::new()
}
/// The Matrix user session.
pub fn session(&self) -> Option<Session> {
self.imp().session.upgrade()
}
/// Set the Matrix user session.
pub fn set_session(&self, session: Option<Session>) {
if self.session() == session {
return;
}
self.imp().session.set(session.as_ref());
self.notify("session");
}
/// The currently selected room, if any.
pub fn selected_room(&self) -> Option<Room> {
self.imp()
.content
.item()
.and_then(|item| item.downcast().ok())
}
pub fn select_room(&self, room: Option<Room>) {
self.select_item(room.map(|item| item.upcast()));
}
pub fn select_item(&self, item: Option<glib::Object>) {
let Some(session) = self.session() else {
return;
};
session
.sidebar_list_model()
.selection_model()
.set_selected_item(item);
}
pub fn select_room_by_id(&self, room_id: &RoomId) {
if let Some(room) = self.session().and_then(|s| s.room_list().get(room_id)) {
self.select_room(Some(room));
} else {
warn!("A room with id {room_id} couldn't be found");
}
}
fn toggle_room_search(&self) {
let room_search = self.imp().sidebar.room_search_bar();
room_search.set_search_mode(!room_search.is_search_mode());
}
/// Returns the parent GtkWindow containing this widget.
fn parent_window(&self) -> Option<Window> {
self.root()?.downcast().ok()
}
fn show_room_creation_dialog(&self) {
let Some(session) = self.session() else {
return;
};
let window = RoomCreation::new(self.parent_window().as_ref(), &session);
window.present();
}
fn show_create_dm_dialog(&self) {
let Some(session) = self.session() else {
return;
};
let window = CreateDmDialog::new(self.parent_window().as_ref(), &session);
window.present();
}
async fn show_join_room_dialog(&self) {
let Some(session) = self.session() else {
return;
};
let dialog = JoinRoomDialog::new(self.parent_window().as_ref(), &session);
dialog.present();
}
pub fn handle_paste_action(&self) {
self.imp().content.handle_paste_action();
}
/// Show the content of the session
pub fn show_content(&self) {
let imp = self.imp();
imp.stack.set_visible_child(&*imp.overlay);
if let Some(window) = self.parent_window() {
window.switch_to_session_page();
}
}
/// Show a media event.
pub fn show_media(&self, event: &Event, source_widget: &impl IsA<gtk::Widget>) {
let Some(message) = event.message() else {
error!("Trying to open the media viewer with an event that is not a message");
return;
};
let imp = self.imp();
imp.media_viewer
.set_message(&event.room(), event.event_id().unwrap(), message);
imp.media_viewer.reveal(source_widget);
}
}

92
src/window.rs

@ -12,7 +12,7 @@ use crate::{
components::Spinner,
config::{APP_ID, PROFILE},
secret::{self, SecretError, StoredSession},
session::SessionState,
session::{AccountSettings, SessionState, SessionView},
session_list::SessionList,
spawn, spawn_tokio, toast,
user_facing_error::UserFacingError,
@ -39,7 +39,7 @@ mod imp {
#[template_child]
pub error_page: TemplateChild<ErrorPage>,
#[template_child]
pub sessions: TemplateChild<gtk::Stack>,
pub session: TemplateChild<SessionView>,
#[template_child]
pub toast_overlay: TemplateChild<adw::ToastOverlay>,
#[template_child]
@ -77,16 +77,19 @@ mod imp {
"win.paste",
None,
);
klass.install_action("win.paste", None, move |widget, _, _| {
if let Some(session) = widget
.imp()
.sessions
.visible_child()
.and_then(|c| c.downcast::<Session>().ok())
{
session.handle_paste_action();
}
klass.install_action("win.paste", None, move |obj, _, _| {
obj.imp().session.handle_paste_action();
});
klass.install_action(
"win.open-account-settings",
Some("s"),
move |obj, _, variant| {
if let Some(session_id) = variant.and_then(|v| v.get::<String>()) {
obj.open_account_settings(&session_id);
}
},
);
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -153,14 +156,6 @@ mod imp {
self.session_selection.set_model(Some(&self.session_list));
self.session_selection.set_autoselect(true);
self.session_selection.connect_selected_item_notify(
clone!(@weak self as imp => move |session_selection| {
if let Some(session) = session_selection.selected_item().and_downcast::<Session>() {
imp.sessions.set_visible_child(&session);
}
}),
);
spawn!(clone!(@weak obj => async move {
obj.restore_sessions().await;
}));
@ -226,7 +221,6 @@ impl Window {
let imp = &self.imp();
let index = imp.session_list.add(session.clone());
imp.sessions.add_named(session, Some(session.session_id()));
let settings = Application::default().settings();
let mut is_opened = false;
if session.session_id() == settings.string("current-session") {
@ -234,11 +228,11 @@ impl Window {
is_opened = true;
if session.state() == SessionState::Ready {
session.show_content();
imp.session.show_content();
} else {
session.connect_ready(|session| {
session.show_content();
});
session.connect_ready(clone!(@weak self as obj => move |_| {
obj.imp().session.show_content();
}));
self.switch_to_loading_page();
}
} else if imp.waiting_sessions.get() > 0 {
@ -249,16 +243,16 @@ impl Window {
imp.session_selection.set_selected(index as u32);
if session.state() == SessionState::Ready {
session.show_content();
imp.session.show_content();
} else {
session.connect_ready(|session| {
session.show_content();
});
session.connect_ready(clone!(@weak self as obj => move |_| {
obj.imp().session.show_content();
}));
self.switch_to_loading_page();
}
}
// We need to grab the focus so that keyboard shortcuts work
session.grab_focus();
imp.session.grab_focus();
session.connect_logged_out(clone!(@weak self as obj => move |session| {
obj.remove_session(session)
@ -269,7 +263,6 @@ impl Window {
let imp = self.imp();
imp.session_list.remove(session.session_id());
imp.sessions.remove(session);
if imp.session_list.is_empty() {
self.switch_to_greeter_page();
@ -361,14 +354,19 @@ impl Window {
}
/// Set the current session by its ID.
pub fn set_current_session_by_id(&self, session_id: &str) {
///
/// Returns `true` if the session was set as the current session.
pub fn set_current_session_by_id(&self, session_id: &str) -> bool {
let imp = self.imp();
if let Some(index) = imp.session_list.index(session_id) {
imp.session_selection.set_selected(index as u32);
} else {
return false;
}
self.switch_to_sessions_page();
self.switch_to_session_page();
true
}
pub fn save_window_size(&self) -> Result<(), glib::BoolError> {
@ -414,9 +412,9 @@ impl Window {
imp.main_stack.set_visible_child(&*imp.loading);
}
pub fn switch_to_sessions_page(&self) {
pub fn switch_to_session_page(&self) {
let imp = self.imp();
imp.main_stack.set_visible_child(&imp.sessions.get());
imp.main_stack.set_visible_child(&imp.session.get());
}
pub fn switch_to_login_page(&self) {
@ -445,6 +443,11 @@ impl Window {
&self.imp().account_switcher
}
/// The `SessionView` of this window.
pub fn session_view(&self) -> &SessionView {
&self.imp().session
}
fn update_network_state(&self) {
let imp = self.imp();
let monitor = gio::NetworkMonitor::default();
@ -462,13 +465,13 @@ impl Window {
}
}
/// Show the given room for the given session.
pub fn show_room(&self, session_id: &str, room_id: &RoomId) {
if let Some(session) = self.session_list().get(session_id) {
session.select_room_by_id(room_id);
self.set_current_session_by_id(session_id);
}
if self.set_current_session_by_id(session_id) {
self.imp().session.select_room_by_id(room_id);
self.present();
self.present();
}
}
pub fn save_current_visible_session(&self) -> Result<(), glib::BoolError> {
@ -481,4 +484,15 @@ impl Window {
Ok(())
}
/// Open the account settings for the session with the given ID.
pub fn open_account_settings(&self, session_id: &str) {
let Some(session) = self.session_list().get(session_id) else {
error!("Tried to open account settings of unknown session with ID '{session_id}'");
return;
};
let window = AccountSettings::new(Some(self), &session);
window.present();
}
}

Loading…
Cancel
Save