diff --git a/data/resources/icons/scalable/actions/hide-symbolic.svg b/data/resources/icons/scalable/actions/hide-symbolic.svg new file mode 100644 index 00000000..6a4d6fc4 --- /dev/null +++ b/data/resources/icons/scalable/actions/hide-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 310040e3..f5612240 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -18,6 +18,7 @@ icons/scalable/actions/go-bottom-symbolic.svg icons/scalable/actions/go-next-symbolic.svg icons/scalable/actions/go-previous-symbolic.svg + icons/scalable/actions/hide-symbolic.svg icons/scalable/actions/idp-apple-dark.svg icons/scalable/actions/idp-apple.svg icons/scalable/actions/idp-facebook.svg diff --git a/po/POTFILES.in b/po/POTFILES.in index 6b139d01..0e249869 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -159,6 +159,7 @@ src/session/view/content/room_history/message_row/reaction_list.ui src/session/view/content/room_history/message_row/reply.ui src/session/view/content/room_history/message_row/text/widgets.rs src/session/view/content/room_history/message_row/visual_media.rs +src/session/view/content/room_history/message_row/visual_media.ui src/session/view/content/room_history/message_toolbar/attachment_dialog.ui src/session/view/content/room_history/message_toolbar/completion/completion_popover.rs src/session/view/content/room_history/message_toolbar/mod.rs diff --git a/src/session/model/mod.rs b/src/session/model/mod.rs index 3e42e274..72b51622 100644 --- a/src/session/model/mod.rs +++ b/src/session/model/mod.rs @@ -23,7 +23,7 @@ pub(crate) use self::{ room_list::RoomList, security::*, session::*, - session_settings::{SessionSettings, StoredSessionSettings}, + session_settings::*, sidebar_data::{ Selection, SidebarIconItem, SidebarIconItemType, SidebarItemList, SidebarListModel, SidebarSection, SidebarSectionName, diff --git a/src/session/model/notifications/notifications_settings.rs b/src/session/model/notifications/notifications_settings.rs index 7bddb046..970c54e6 100644 --- a/src/session/model/notifications/notifications_settings.rs +++ b/src/session/model/notifications/notifications_settings.rs @@ -38,9 +38,7 @@ pub enum NotificationsGlobalSetting { } /// The possible values for a room notifications setting. -#[derive( - Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum, strum::Display, strum::EnumString, -)] +#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum, strum::EnumString)] #[enum_type(name = "NotificationsRoomSetting")] #[strum(serialize_all = "kebab-case")] pub enum NotificationsRoomSetting { diff --git a/src/session/model/session_settings.rs b/src/session/model/session_settings.rs index be2513cd..c6b733a8 100644 --- a/src/session/model/session_settings.rs +++ b/src/session/model/session_settings.rs @@ -1,16 +1,16 @@ use std::collections::BTreeSet; -use gtk::{glib, prelude::*, subclass::prelude::*}; +use gtk::{glib, glib::closure_local, prelude::*, subclass::prelude::*}; use indexmap::IndexSet; -use ruma::OwnedServerName; +use ruma::{serde::SerializeAsRefStr, OwnedServerName}; use serde::{Deserialize, Serialize}; -use super::SidebarSectionName; -use crate::Application; +use super::{Room, SidebarSectionName}; +use crate::{session_list::SessionListSettings, Application}; -#[derive(Debug, Clone, Serialize, Deserialize, glib::Boxed)] -#[boxed_type(name = "StoredSessionSettings")] -pub struct StoredSessionSettings { +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(clippy::struct_excessive_bools)] +pub(crate) struct StoredSessionSettings { /// Custom servers to explore. #[serde(default, skip_serializing_if = "IndexSet::is_empty")] explore_custom_servers: IndexSet, @@ -39,6 +39,10 @@ pub struct StoredSessionSettings { /// The sections that are expanded. #[serde(default)] sections_expanded: SectionsExpanded, + + /// Which rooms display media previews for this session. + #[serde(default, skip_serializing_if = "ruma::serde::is_default")] + media_previews_enabled: MediaPreviewsSetting, } impl Default for StoredSessionSettings { @@ -49,6 +53,7 @@ impl Default for StoredSessionSettings { public_read_receipts_enabled: true, typing_enabled: true, sections_expanded: Default::default(), + media_previews_enabled: Default::default(), } } } @@ -57,8 +62,11 @@ mod imp { use std::{ cell::{OnceCell, RefCell}, marker::PhantomData, + sync::LazyLock, }; + use glib::subclass::Signal; + use super::*; #[derive(Debug, Default, glib::Properties)] @@ -66,19 +74,18 @@ mod imp { pub struct SessionSettings { /// The ID of the session these settings are for. #[property(get, construct_only)] - pub session_id: OnceCell, + session_id: OnceCell, /// The stored settings. - #[property(get, construct_only)] - pub stored_settings: RefCell, + pub(super) stored_settings: RefCell, /// Whether notifications are enabled for this session. #[property(get = Self::notifications_enabled, set = Self::set_notifications_enabled, explicit_notify, default = true)] - pub notifications_enabled: PhantomData, + notifications_enabled: PhantomData, /// Whether public read receipts are enabled for this session. #[property(get = Self::public_read_receipts_enabled, set = Self::set_public_read_receipts_enabled, explicit_notify, default = true)] - pub public_read_receipts_enabled: PhantomData, + public_read_receipts_enabled: PhantomData, /// Whether typing notifications are enabled for this session. #[property(get = Self::typing_enabled, set = Self::set_typing_enabled, explicit_notify, default = true)] - pub typing_enabled: PhantomData, + typing_enabled: PhantomData, } #[glib::object_subclass] @@ -88,7 +95,13 @@ mod imp { } #[glib::derived_properties] - impl ObjectImpl for SessionSettings {} + impl ObjectImpl for SessionSettings { + fn signals() -> &'static [Signal] { + static SIGNALS: LazyLock> = + LazyLock::new(|| vec![Signal::builder("media-previews-enabled-changed").build()]); + SIGNALS.as_ref() + } + } impl SessionSettings { /// Whether notifications are enabled for this session. @@ -103,7 +116,7 @@ mod imp { } self.stored_settings.borrow_mut().notifications_enabled = enabled; - super::SessionSettings::save(); + session_list_settings().save(); self.obj().notify_notifications_enabled(); } @@ -121,7 +134,7 @@ mod imp { self.stored_settings .borrow_mut() .public_read_receipts_enabled = enabled; - super::SessionSettings::save(); + session_list_settings().save(); self.obj().notify_public_read_receipts_enabled(); } @@ -137,7 +150,7 @@ mod imp { } self.stored_settings.borrow_mut().typing_enabled = enabled; - super::SessionSettings::save(); + session_list_settings().save(); self.obj().notify_typing_enabled(); } } @@ -150,33 +163,30 @@ glib::wrapper! { impl SessionSettings { /// Create a new `SessionSettings` for the given session ID. - pub fn new(session_id: &str) -> Self { + pub(crate) fn new(session_id: &str) -> Self { glib::Object::builder() .property("session-id", session_id) - .property("stored-settings", StoredSessionSettings::default()) .build() } /// Restore existing `SessionSettings` with the given session ID and stored /// settings. - pub fn restore(session_id: &str, stored_settings: &StoredSessionSettings) -> Self { - glib::Object::builder() + pub(crate) fn restore(session_id: &str, stored_settings: StoredSessionSettings) -> Self { + let obj = glib::Object::builder::() .property("session-id", session_id) - .property("stored-settings", stored_settings) - .build() + .build(); + *obj.imp().stored_settings.borrow_mut() = stored_settings; + obj } - /// Save these settings in the application settings. - fn save() { - Application::default().session_list().settings().save(); + /// The stored settings. + pub(crate) fn stored_settings(&self) -> StoredSessionSettings { + self.imp().stored_settings.borrow().clone() } /// Delete the settings from the application settings. - pub fn delete(&self) { - Application::default() - .session_list() - .settings() - .remove(&self.session_id()); + pub(crate) fn delete(&self) { + session_list_settings().remove(&self.session_id()); } /// Custom servers to explore. @@ -189,7 +199,7 @@ impl SessionSettings { } /// Set the custom servers to explore. - pub fn set_explore_custom_servers(&self, servers: IndexSet) { + pub(crate) fn set_explore_custom_servers(&self, servers: IndexSet) { if self.explore_custom_servers() == servers { return; } @@ -198,11 +208,11 @@ impl SessionSettings { .stored_settings .borrow_mut() .explore_custom_servers = servers; - Self::save(); + session_list_settings().save(); } /// Whether the section with the given name is expanded. - pub fn is_section_expanded(&self, section_name: SidebarSectionName) -> bool { + pub(crate) fn is_section_expanded(&self, section_name: SidebarSectionName) -> bool { self.imp() .stored_settings .borrow() @@ -211,28 +221,75 @@ impl SessionSettings { } /// Set whether the section with the given name is expanded. - pub fn set_section_expanded(&self, section_name: SidebarSectionName, expanded: bool) { + pub(crate) fn set_section_expanded(&self, section_name: SidebarSectionName, expanded: bool) { self.imp() .stored_settings .borrow_mut() .sections_expanded .set_section_expanded(section_name, expanded); - Self::save(); + session_list_settings().save(); + } + + /// Whether the given room should display media previews. + pub(crate) fn should_room_show_media_previews(&self, room: &Room) -> bool { + self.imp() + .stored_settings + .borrow() + .media_previews_enabled + .should_room_show_media_previews(room) + } + + /// Which rooms display media previews. + pub(crate) fn media_previews_global_enabled(&self) -> MediaPreviewsGlobalSetting { + self.imp() + .stored_settings + .borrow() + .media_previews_enabled + .global + } + + /// Set which rooms display media previews. + pub(crate) fn set_media_previews_global_enabled(&self, setting: MediaPreviewsGlobalSetting) { + self.imp() + .stored_settings + .borrow_mut() + .media_previews_enabled + .global = setting; + session_list_settings().save(); + self.emit_by_name::<()>("media-previews-enabled-changed", &[]); + } + + /// Connect to the signal emitted when the media previews setting changed. + pub fn connect_media_previews_enabled_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_closure( + "media-previews-enabled-changed", + true, + closure_local!(move |obj: Self| { + f(&obj); + }), + ) } } /// The sections that are expanded. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub struct SectionsExpanded(BTreeSet); +pub(crate) struct SectionsExpanded(BTreeSet); impl SectionsExpanded { /// Whether the section with the given name is expanded. - pub fn is_section_expanded(&self, section_name: SidebarSectionName) -> bool { + pub(crate) fn is_section_expanded(&self, section_name: SidebarSectionName) -> bool { self.0.contains(§ion_name) } /// Set whether the section with the given name is expanded. - pub fn set_section_expanded(&mut self, section_name: SidebarSectionName, expanded: bool) { + pub(crate) fn set_section_expanded( + &mut self, + section_name: SidebarSectionName, + expanded: bool, + ) { if expanded { self.0.insert(section_name); } else { @@ -252,3 +309,69 @@ impl Default for SectionsExpanded { ])) } } + +/// Setting about which rooms display media previews. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct MediaPreviewsSetting { + /// The default setting for all rooms. + #[serde(default, skip_serializing_if = "ruma::serde::is_default")] + global: MediaPreviewsGlobalSetting, +} + +impl MediaPreviewsSetting { + // Whether the given room should show room previews according to this setting. + pub(crate) fn should_room_show_media_previews(&self, room: &Room) -> bool { + self.global.should_room_show_media_previews(room) + } +} + +/// Possible values of the global setting about which rooms display media +/// previews. +#[derive( + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + strum::AsRefStr, + strum::EnumString, + SerializeAsRefStr, +)] +#[strum(serialize_all = "kebab-case")] +pub(crate) enum MediaPreviewsGlobalSetting { + /// All rooms show media previews. + All, + /// Only private rooms show media previews. + #[default] + Private, + /// No rooms show media previews. + None, +} + +impl MediaPreviewsGlobalSetting { + /// Whether the given room should show room previews according to this + /// setting. + pub(crate) fn should_room_show_media_previews(self, room: &Room) -> bool { + match self { + Self::All => true, + Self::Private => !room.join_rule().anyone_can_join(), + Self::None => false, + } + } +} + +impl<'de> Deserialize<'de> for MediaPreviewsGlobalSetting { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let cow = ruma::serde::deserialize_cow_str(deserializer)?; + cow.parse().map_err(serde::de::Error::custom) + } +} + +/// The session list settings of the application. +fn session_list_settings() -> SessionListSettings { + Application::default().session_list().settings() +} diff --git a/src/session/view/account_settings/safety_page/mod.rs b/src/session/view/account_settings/safety_page/mod.rs index da7c0e34..6670177d 100644 --- a/src/session/view/account_settings/safety_page/mod.rs +++ b/src/session/view/account_settings/safety_page/mod.rs @@ -1,13 +1,17 @@ use adw::{prelude::*, subclass::prelude::*}; use gtk::{glib, glib::clone, CompositeTemplate}; +use tracing::error; mod ignored_users_subpage; pub(super) use self::ignored_users_subpage::IgnoredUsersSubpage; -use crate::{components::ButtonCountRow, session::model::Session}; +use crate::{ + components::ButtonCountRow, + session::model::{MediaPreviewsGlobalSetting, Session}, +}; mod imp { - use std::cell::RefCell; + use std::{cell::RefCell, marker::PhantomData}; use glib::subclass::InitializingObject; @@ -26,7 +30,11 @@ mod imp { /// The current session. #[property(get, set = Self::set_session, nullable)] session: glib::WeakRef, + /// The media previews setting, as a string. + #[property(get = Self::media_previews_enabled, set = Self::set_media_previews_enabled)] + media_previews_enabled: PhantomData, ignored_users_count_handler: RefCell>, + session_settings_handler: RefCell>, bindings: RefCell>, } @@ -38,6 +46,11 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); + + klass.install_property_action( + "safety.set-media-previews-enabled", + "media-previews-enabled", + ); } fn instance_init(obj: &InitializingObject) { @@ -48,15 +61,7 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for SafetyPage { fn dispose(&self) { - if let Some(session) = self.session.upgrade() { - if let Some(handler) = self.ignored_users_count_handler.take() { - session.ignored_users().disconnect(handler); - } - } - - for binding in self.bindings.take() { - binding.unbind(); - } + self.clear(); } } @@ -66,20 +71,12 @@ mod imp { impl SafetyPage { /// Set the current session. fn set_session(&self, session: Option<&Session>) { - let prev_session = self.session.upgrade(); - - if prev_session.as_ref() == session { + if self.session.upgrade().as_ref() == session { return; } - if let Some(session) = prev_session { - if let Some(handler) = self.ignored_users_count_handler.take() { - session.ignored_users().disconnect(handler); - } - } - for binding in self.bindings.take() { - binding.unbind(); - } + self.clear(); + let obj = self.obj(); if let Some(session) = session { let ignored_users = session.ignored_users(); @@ -99,6 +96,18 @@ mod imp { let session_settings = session.settings(); + let media_previews_handler = session_settings + .connect_media_previews_enabled_changed(clone!( + #[weak] + obj, + move |_| { + // Update the active media previews radio button. + obj.notify_media_previews_enabled(); + } + )); + self.session_settings_handler + .replace(Some(media_previews_handler)); + let public_read_receipts_binding = session_settings .bind_property( "public-read-receipts-enabled", @@ -119,7 +128,56 @@ mod imp { } self.session.set(session); - self.obj().notify_session(); + + // Update the active media previews radio button. + obj.notify_media_previews_enabled(); + obj.notify_session(); + } + + /// The media previews setting, as a string. + fn media_previews_enabled(&self) -> String { + let Some(session) = self.session.upgrade() else { + return String::new(); + }; + + session + .settings() + .media_previews_global_enabled() + .as_ref() + .to_owned() + } + + /// Set the media previews setting, as a string. + fn set_media_previews_enabled(&self, setting: &str) { + let Some(session) = self.session.upgrade() else { + return; + }; + + let Ok(setting) = setting.parse::() else { + error!("Invalid value to set global media previews setting: {setting}"); + return; + }; + + session + .settings() + .set_media_previews_global_enabled(setting); + } + + /// Reset the signal handlers and bindings. + fn clear(&self) { + if let Some(session) = self.session.upgrade() { + if let Some(handler) = self.ignored_users_count_handler.take() { + session.ignored_users().disconnect(handler); + } + + if let Some(handler) = self.session_settings_handler.take() { + session.settings().disconnect(handler); + } + } + + for binding in self.bindings.take() { + binding.unbind(); + } } } } diff --git a/src/session/view/account_settings/safety_page/mod.ui b/src/session/view/account_settings/safety_page/mod.ui index 5ed8c2f6..4288a161 100644 --- a/src/session/view/account_settings/safety_page/mod.ui +++ b/src/session/view/account_settings/safety_page/mod.ui @@ -35,5 +35,32 @@ + + + Media Previews + Which rooms automatically show previews for images and videos. Hidden previews can always be shown by clicking on the media. + + + Show in all rooms + safety.set-media-previews-enabled + 'all' + + + + + Show only in private rooms + safety.set-media-previews-enabled + 'private' + + + + + Hide in all rooms + safety.set-media-previews-enabled + 'none' + + + + diff --git a/src/session/view/content/room_history/message_row/content.rs b/src/session/view/content/room_history/message_row/content.rs index 363cfcba..bf87d628 100644 --- a/src/session/view/content/room_history/message_row/content.rs +++ b/src/session/view/content/room_history/message_row/content.rs @@ -12,7 +12,7 @@ use super::{ use crate::{ prelude::*, session::{ - model::{Event, Member, Room, Session}, + model::{Event, Member, Room}, view::content::room_history::message_toolbar::MessageEventSource, }, spawn, @@ -380,10 +380,6 @@ trait MessageContentContainer: IsA { detect_at_room: bool, cache_key: MessageCacheKey, ) { - let Some(session) = room.session() else { - return; - }; - if let Some((caption, formatted_caption)) = media_message.caption() { let caption_widget = self.reuse_child_or_default::(); @@ -395,9 +391,9 @@ trait MessageContentContainer: IsA { detect_at_room, ); - caption_widget.build_media_content(media_message, format, &session, cache_key); + caption_widget.build_media_content(media_message, format, room, cache_key); } else { - self.build_media_content(media_message, format, &session, cache_key); + self.build_media_content(media_message, format, room, cache_key); } } @@ -409,13 +405,16 @@ trait MessageContentContainer: IsA { &self, media_message: MediaMessage, format: ContentFormat, - session: &Session, + room: &Room, cache_key: MessageCacheKey, ) { match media_message { MediaMessage::Audio(audio) => { + let Some(session) = room.session() else { + return; + }; let widget = self.reuse_child_or_default::(); - widget.audio(audio.into(), session, format, cache_key); + widget.audio(audio.into(), &session, format, cache_key); } MediaMessage::File(file) => { let widget = self.reuse_child_or_default::(); @@ -426,15 +425,15 @@ trait MessageContentContainer: IsA { } MediaMessage::Image(image) => { let widget = self.reuse_child_or_default::(); - widget.set_media_message(image.into(), session, format, cache_key); + widget.set_media_message(image.into(), room, format, cache_key); } MediaMessage::Video(video) => { let widget = self.reuse_child_or_default::(); - widget.set_media_message(video.into(), session, format, cache_key); + widget.set_media_message(video.into(), room, format, cache_key); } MediaMessage::Sticker(sticker) => { let widget = self.reuse_child_or_default::(); - widget.set_media_message(sticker.into(), session, format, cache_key); + widget.set_media_message(sticker.into(), room, format, cache_key); } } } diff --git a/src/session/view/content/room_history/message_row/visual_media.rs b/src/session/view/content/room_history/message_row/visual_media.rs index 41772979..4a8fa07c 100644 --- a/src/session/view/content/room_history/message_row/visual_media.rs +++ b/src/session/view/content/room_history/message_row/visual_media.rs @@ -1,11 +1,6 @@ use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; -use gtk::{ - gdk, - glib::{self, clone}, - CompositeTemplate, -}; -use matrix_sdk::Client; +use gtk::{gdk, glib, glib::clone, CompositeTemplate}; use ruma::api::client::media::get_content_thumbnail::v3::Method; use tracing::{error, warn}; @@ -13,15 +8,15 @@ use super::{content::MessageCacheKey, ContentFormat}; use crate::{ components::{AnimatedImagePaintable, VideoPlayer}, gettext_f, - session::model::Session, + session::model::Room, spawn, utils::{ - matrix::VisualMediaMessage, + matrix::{VisualMediaMessage, VisualMediaType}, media::{ image::{ImageRequestPriority, ThumbnailSettings, THUMBNAIL_MAX_DIMENSIONS}, FrameDimensions, }, - CountedRef, File, LoadingState, + CountedRef, File, LoadingState, TemplateCallbacks, }, }; @@ -35,6 +30,8 @@ const MAX_COMPACT_DIMENSIONS: FrameDimensions = FrameDimensions { width: 75, height: 50, }; +/// The name of the placeholder stack page. +const PLACEHOLDER_PAGE: &str = "placeholder"; /// The name of the media stack page. const MEDIA_PAGE: &str = "media"; @@ -56,11 +53,26 @@ mod imp { #[template_child] stack: TemplateChild, #[template_child] + preview_instructions: TemplateChild, + #[template_child] + preview_instructions_icon: TemplateChild, + #[template_child] spinner: TemplateChild, #[template_child] + hide_preview_button: TemplateChild, + #[template_child] error: TemplateChild, - /// The supposed dimensions of the media. - dimensions: Cell>, + /// The room where the message was sent. + room: glib::WeakRef, + join_rule_handler: RefCell>, + session_settings_handler: RefCell>, + /// The visual media message to display. + media_message: RefCell>, + /// The cache key for the current media message. + /// + /// We only try to reload the media if the key changes. This is to avoid + /// reloading the media when a local echo changes to a remote echo. + cache_key: RefCell, /// The loading state of the media. #[property(get, builder(LoadingState::default()))] state: Cell, @@ -74,11 +86,6 @@ mod imp { #[property(get)] activatable: Cell, gesture_click: glib::WeakRef, - /// The cache key for the current media message. - /// - /// We only try to reload the media if the key changes. This is to avoid - /// reloading the media when a local echo changes to a remote echo. - cache_key: RefCell, /// The current video file, if any. file: RefCell>, paintable_animation_ref: RefCell>, @@ -92,6 +99,8 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); + Self::bind_template_callbacks(klass); + TemplateCallbacks::bind_template_callbacks(klass); klass.set_css_name("message-visual-media"); klass.set_accessible_role(gtk::AccessibleRole::Group); @@ -105,6 +114,7 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for MessageVisualMedia { fn dispose(&self) { + self.clear(); self.overlay.unparent(); } } @@ -171,7 +181,12 @@ mod imp { }; // Use the size from the info or the fallback size. - let media_size = self.dimensions.get().unwrap_or(FALLBACK_DIMENSIONS); + let media_size = self + .media_message + .borrow() + .as_ref() + .and_then(VisualMediaMessage::dimensions) + .unwrap_or(FALLBACK_DIMENSIONS); let nat = media_size .scale_to_fit(wanted_size, gtk::ContentFit::ScaleDown) .dimension_for_orientation(orientation) @@ -200,6 +215,7 @@ mod imp { } } + #[gtk::template_callbacks] impl MessageVisualMedia { /// The media child of the given type, if any. pub(super) fn media_child>(&self) -> Option { @@ -209,12 +225,14 @@ mod imp { /// Set the media child. /// /// Removes the previous media child if one was set. - fn set_media_child(&self, child: &impl IsA) { + fn set_media_child(&self, child: Option<&impl IsA>) { if let Some(prev_child) = self.stack.child_by_name(MEDIA_PAGE) { self.stack.remove(&prev_child); } - self.stack.add_named(child, Some(MEDIA_PAGE)); + if let Some(child) = child { + self.stack.add_named(child, Some(MEDIA_PAGE)); + } } /// Set the state of the media. @@ -223,27 +241,42 @@ mod imp { return; } - match state { - LoadingState::Loading | LoadingState::Initial => { - self.stack.set_visible_child_name("placeholder"); - self.spinner.set_visible(true); - self.error.set_visible(false); - } - LoadingState::Ready => { - self.stack.set_visible_child_name(MEDIA_PAGE); - self.spinner.set_visible(false); - self.error.set_visible(false); - } - LoadingState::Error => { - self.spinner.set_visible(false); - self.error.set_visible(true); - } - } - self.state.set(state); + + self.update_visible_page(); self.obj().notify_state(); } + /// Update the visible page for the current state. + fn update_visible_page(&self) { + let Some(room) = self.room.upgrade() else { + return; + }; + let Some(session) = room.session() else { + return; + }; + + let state = self.state.get(); + + self.preview_instructions + .set_visible(state == LoadingState::Initial); + self.spinner.set_visible(state == LoadingState::Loading); + self.hide_preview_button.set_visible( + state == LoadingState::Ready + && !session.settings().should_room_show_media_previews(&room), + ); + self.error.set_visible(state == LoadingState::Error); + + let visible_page = match state { + LoadingState::Initial | LoadingState::Loading => Some(PLACEHOLDER_PAGE), + LoadingState::Ready => Some(MEDIA_PAGE), + LoadingState::Error => None, + }; + if let Some(visible_page) = visible_page { + self.stack.set_visible_child_name(visible_page); + } + } + /// Update the state of the animated paintable, if any. fn update_animated_paintable_state(&self) { self.paintable_animation_ref.take(); @@ -276,6 +309,13 @@ mod imp { self.overlay.remove_css_class("compact"); } + let icon_size = if compact { + gtk::IconSize::Normal + } else { + gtk::IconSize::Large + }; + self.preview_instructions_icon.set_icon_size(icon_size); + self.update_gesture_click(); self.obj().notify_compact(); } @@ -304,7 +344,9 @@ mod imp { #[weak(rename_to = imp)] self, move |_, _, _, _| { - if imp + if imp.state.get() == LoadingState::Initial { + imp.show_media(); + } else if imp .obj() .activate_action("message-row.show-media", None) .is_err() @@ -333,11 +375,11 @@ mod imp { should_reload } - /// Build the content for the given media message. - pub(super) fn build( + /// Set the visual media message to display. + pub(super) fn set_media_message( &self, media_message: VisualMediaMessage, - session: &Session, + room: &Room, format: ContentFormat, cache_key: MessageCacheKey, ) { @@ -346,44 +388,136 @@ mod imp { return; } - self.file.take(); - self.dimensions.set(media_message.dimensions()); + // Reset the widget. + self.clear(); + self.set_state(LoadingState::Initial); let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized); self.set_compact(compact); - let activatable = matches!( - media_message, - VisualMediaMessage::Image(_) | VisualMediaMessage::Video(_) - ); - self.set_activatable(activatable); + let Some(session) = room.session() else { + return; + }; + + let join_rule_handler = room.join_rule().connect_anyone_can_join_notify(clone!( + #[weak(rename_to = imp)] + self, + move |_| { + imp.update_media(); + } + )); + self.join_rule_handler.replace(Some(join_rule_handler)); + + let session_settings_handler = session + .settings() + .connect_media_previews_enabled_changed(clone!( + #[weak(rename_to = imp)] + self, + move |_| { + imp.update_media(); + } + )); + self.session_settings_handler + .replace(Some(session_settings_handler)); + + self.room.set(Some(room)); + self.media_message.replace(Some(media_message)); + + self.update_accessible_label(); + self.update_preview_instructions_icon(); + self.update_media(); + } + + /// Update the accessible label for the current state. + fn update_accessible_label(&self) { + let Some((filename, visual_media_type)) = + self.media_message.borrow().as_ref().map(|media_message| { + (media_message.filename(), media_message.visual_media_type()) + }) + else { + return; + }; - let filename = media_message.filename(); let accessible_label = if filename.is_empty() { - match &media_message { - VisualMediaMessage::Image(_) => gettext("Image"), - VisualMediaMessage::Sticker(_) => gettext("Sticker"), - VisualMediaMessage::Video(_) => gettext("Video"), + match visual_media_type { + VisualMediaType::Image => gettext("Image"), + VisualMediaType::Sticker => gettext("Sticker"), + VisualMediaType::Video => gettext("Video"), } } else { - match &media_message { - VisualMediaMessage::Image(_) => { + match visual_media_type { + VisualMediaType::Image => { gettext_f("Image: {filename}", &[("filename", &filename)]) } - VisualMediaMessage::Sticker(_) => { + VisualMediaType::Sticker => { gettext_f("Sticker: {filename}", &[("filename", &filename)]) } - VisualMediaMessage::Video(_) => { + VisualMediaType::Video => { gettext_f("Video: {filename}", &[("filename", &filename)]) } } }; self.obj() .update_property(&[gtk::accessible::Property::Label(&accessible_label)]); + } + + /// Update the preview instructions icon for the current state. + fn update_preview_instructions_icon(&self) { + let Some(content_type) = self + .media_message + .borrow() + .as_ref() + .map(VisualMediaMessage::content_type) + else { + return; + }; + + self.preview_instructions_icon + .set_icon_name(Some(content_type.icon_name())); + } + + /// Update the media for the current state. + fn update_media(&self) { + let Some(room) = self.room.upgrade() else { + return; + }; + let Some(session) = room.session() else { + return; + }; + + if session.settings().should_room_show_media_previews(&room) { + // Only load the media if it was not loaded before. + if self.state.get() == LoadingState::Initial { + self.show_media(); + } + } else { + self.hide_media(); + } + } + + /// Hide the media. + #[template_callback] + fn hide_media(&self) { + self.set_state(LoadingState::Initial); + self.set_media_child(None::<>k::Widget>); + self.file.take(); + self.set_activatable(true); + } + + /// Show the media. + fn show_media(&self) { + let Some(media_message) = self.media_message.borrow().clone() else { + return; + }; self.set_state(LoadingState::Loading); - let client = session.client(); + let activatable = matches!( + media_message, + VisualMediaMessage::Image(_) | VisualMediaMessage::Video(_) + ); + self.set_activatable(activatable); + spawn!( glib::Priority::LOW, clone!( @@ -392,10 +526,10 @@ mod imp { async move { match &media_message { VisualMediaMessage::Image(_) | VisualMediaMessage::Sticker(_) => { - imp.build_image(&media_message, client).await; + imp.build_image(&media_message).await; } VisualMediaMessage::Video(_) => { - imp.build_video(media_message, &client).await; + imp.build_video(media_message).await; } } @@ -406,14 +540,27 @@ mod imp { } /// Build the content for the image in the given media message. - async fn build_image(&self, media_message: &VisualMediaMessage, client: Client) { + async fn build_image(&self, media_message: &VisualMediaMessage) { + let Some(client) = self + .room + .upgrade() + .and_then(|room| room.session()) + .map(|session| session.client()) + else { + return; + }; + + if self.state.get() != LoadingState::Loading { + // Something occurred after the task was spawned, cancel the task. + return; + } + // Disable the copy-image action while the image is loading. if matches!(media_message, VisualMediaMessage::Image(_)) { self.enable_copy_image_action(false); } let scale_factor = self.obj().scale_factor(); - let settings = ThumbnailSettings { dimensions: FrameDimensions::thumbnail_max_dimensions(scale_factor), method: Method::Scale, @@ -433,13 +580,18 @@ mod imp { } }; + if self.state.get() != LoadingState::Loading { + // Something occurred while the image was loading, cancel the task. + return; + } + let child = if let Some(child) = self.media_child::() { child } else { let child = gtk::Picture::builder() .content_fit(gtk::ContentFit::ScaleDown) .build(); - self.set_media_child(&child); + self.set_media_child(Some(&child)); child }; child.set_paintable(Some(&gdk::Paintable::from(image))); @@ -479,8 +631,22 @@ mod imp { } /// Build the content for the video in the given media message. - async fn build_video(&self, media_message: VisualMediaMessage, client: &Client) { - let file = match media_message.into_tmp_file(client).await { + async fn build_video(&self, media_message: VisualMediaMessage) { + let Some(client) = self + .room + .upgrade() + .and_then(|room| room.session()) + .map(|session| session.client()) + else { + return; + }; + + if self.state.get() != LoadingState::Loading { + // Something occurred after the task was spawned, cancel the task. + return; + } + + let file = match media_message.into_tmp_file(&client).await { Ok(file) => file, Err(error) => { warn!("Could not retrieve video: {error}"); @@ -489,6 +655,11 @@ mod imp { } }; + if self.state.get() != LoadingState::Loading { + // Something occurred while the video was loading, cancel the task. + return; + } + let child = if let Some(child) = self.media_child::() { child } else { @@ -500,7 +671,7 @@ mod imp { imp.video_state_changed(player); } )); - self.set_media_child(&child); + self.set_media_child(Some(&child)); child }; @@ -533,6 +704,23 @@ mod imp { } } } + + /// Reset the state of this widget. + fn clear(&self) { + self.file.take(); + + if let Some(room) = self.room.upgrade() { + if let Some(handler) = self.join_rule_handler.take() { + room.join_rule().disconnect(handler); + } + + if let Some(handler) = self.session_settings_handler.take() { + if let Some(session) = room.session() { + session.settings().disconnect(handler); + } + } + } + } } } @@ -548,15 +736,16 @@ impl MessageVisualMedia { glib::Object::new() } - /// Display the given visual media message. + /// Set the visual media message to display. pub(crate) fn set_media_message( &self, media_message: VisualMediaMessage, - session: &Session, + room: &Room, format: ContentFormat, cache_key: MessageCacheKey, ) { - self.imp().build(media_message, session, format, cache_key); + self.imp() + .set_media_message(media_message, room, format, cache_key); } /// Get the texture displayed by this widget, if any. diff --git a/src/session/view/content/room_history/message_row/visual_media.ui b/src/session/view/content/room_history/message_row/visual_media.ui index 5e235242..739bb32e 100644 --- a/src/session/view/content/room_history/message_row/visual_media.ui +++ b/src/session/view/content/room_history/message_row/visual_media.ui @@ -26,8 +26,41 @@ + + + vertical + 6 + center + center + + true + + + + + large + presentation + + + + + + + ContentMessageVisualMedia + + + Click to show preview + True + word-char + + + + + False center center @@ -35,12 +68,28 @@ + + + + False + end + start + 3 + 3 + hide-symbolic + Hide media preview + + + + False center center error-symbolic diff --git a/src/session/view/media_viewer.rs b/src/session/view/media_viewer.rs index 67a212a2..bbeebbbc 100644 --- a/src/session/view/media_viewer.rs +++ b/src/session/view/media_viewer.rs @@ -5,7 +5,7 @@ use ruma::OwnedEventId; use tracing::warn; use crate::{ - components::{ContentType, MediaContentViewer, ScaleRevealer}, + components::{MediaContentViewer, ScaleRevealer}, session::model::Room, spawn, toast, utils::matrix::VisualMediaMessage, @@ -369,10 +369,7 @@ mod imp { return; }; - let content_type = match &message { - VisualMediaMessage::Image(_) | VisualMediaMessage::Sticker(_) => ContentType::Image, - VisualMediaMessage::Video(_) => ContentType::Video, - }; + let content_type = message.content_type(); let client = session.client(); match message.into_tmp_file(&client).await { diff --git a/src/session_list/session_list_settings.rs b/src/session_list/session_list_settings.rs index e8031d39..0fac837f 100644 --- a/src/session_list/session_list_settings.rs +++ b/src/session_list/session_list_settings.rs @@ -66,7 +66,7 @@ impl SessionListSettings { needs_update = true; } - let session = SessionSettings::restore(&session_id, &stored_session); + let session = SessionSettings::restore(&session_id, stored_session); (session_id, session) }) .collect(); diff --git a/src/utils/matrix/media_message.rs b/src/utils/matrix/media_message.rs index 43af712f..c0a37b46 100644 --- a/src/utils/matrix/media_message.rs +++ b/src/utils/matrix/media_message.rs @@ -11,6 +11,7 @@ use ruma::events::{ use tracing::{debug, error}; use crate::{ + components::ContentType, prelude::*, toast, utils::{ @@ -244,6 +245,23 @@ impl VisualMediaMessage { FrameDimensions::from_options(width, height) } + /// The type of the media. + pub(crate) fn visual_media_type(&self) -> VisualMediaType { + match self { + Self::Image(_) => VisualMediaType::Image, + Self::Sticker(_) => VisualMediaType::Sticker, + Self::Video(_) => VisualMediaType::Video, + } + } + + /// The content type of the media. + pub(crate) fn content_type(&self) -> ContentType { + match self { + Self::Image(_) | Self::Sticker(_) => ContentType::Image, + Self::Video(_) => ContentType::Video, + } + } + /// Fetch a thumbnail of the media with the given client and thumbnail /// settings. /// @@ -358,3 +376,14 @@ impl From for MediaMessage { } } } + +/// The type of a visual media message. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum VisualMediaType { + /// An image. + Image, + /// A video. + Video, + /// A sticker. + Sticker, +} diff --git a/src/utils/matrix/mod.rs b/src/utils/matrix/mod.rs index 6126ff7d..5a57e08c 100644 --- a/src/utils/matrix/mod.rs +++ b/src/utils/matrix/mod.rs @@ -36,7 +36,7 @@ use tracing::error; pub(crate) mod ext_traits; mod media_message; -pub(crate) use self::media_message::{MediaMessage, VisualMediaMessage}; +pub(crate) use self::media_message::*; use crate::{ components::Pill, gettext_f,