Merge branch 'kcommaille/safety-pills' into 'main'

room-history: Make sure that mentions respect safety settings

See merge request World/fractal!2024
This commit is contained in:
Kévin Commaille 2025-05-21 10:42:27 +00:00
commit 6bf856ef0d
19 changed files with 490 additions and 349 deletions

View File

@ -16,9 +16,33 @@ pub use self::{
};
use crate::{
components::AnimatedImagePaintable,
session::model::Room,
utils::{BoundObject, BoundObjectWeakRef, CountedRef},
};
/// The safety setting to watch to decide whether the image of the avatar should
/// be displayed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, glib::Enum, Default)]
#[enum_type(name = "AvatarImageSafetySetting")]
pub enum AvatarImageSafetySetting {
/// No setting needs to be watched, the image is always shown when
/// available.
#[default]
None,
/// The media previews safety setting should be watched, with the image only
/// shown when allowed.
///
/// This setting also requires the [`Room`] where the avatar is presented.
MediaPreviews,
/// The invite avatars safety setting should be watched, with the image only
/// shown when allowed.
///
/// This setting also requires the [`Room`] where the avatar is presented.
InviteAvatars,
}
mod imp {
use std::{
cell::{Cell, RefCell},
@ -44,13 +68,19 @@ mod imp {
/// The size of the Avatar.
#[property(get = Self::size, set = Self::set_size, explicit_notify, builder().default_value(-1).minimum(-1))]
size: PhantomData<i32>,
/// Whether to inhibit the image of the avatar.
/// The safety setting to watch to decide whether the image of the
/// avatar should be displayed.
#[property(get, set = Self::set_watched_safety_setting, explicit_notify, builder(AvatarImageSafetySetting::default()))]
watched_safety_setting: Cell<AvatarImageSafetySetting>,
/// The room to watch to apply the current safety settings.
///
/// If the image is inhibited, it will not be loaded.
#[property(get, set = Self::set_inhibit_image, explicit_notify)]
inhibit_image: Cell<bool>,
/// This is required if `watched_safety_setting` is not `None`.
#[property(get, set = Self::set_watched_room, explicit_notify, nullable)]
watched_room: RefCell<Option<Room>>,
paintable_ref: RefCell<Option<CountedRef>>,
paintable_animation_ref: RefCell<Option<CountedRef>>,
watched_room_handler: RefCell<Option<glib::SignalHandlerId>>,
watched_session_settings_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
@ -74,7 +104,11 @@ mod imp {
}
#[glib::derived_properties]
impl ObjectImpl for Avatar {}
impl ObjectImpl for Avatar {
fn dispose(&self) {
self.disconnect_safety_setting_signals();
}
}
impl WidgetImpl for Avatar {
fn map(&self) {
@ -116,16 +150,134 @@ mod imp {
self.obj().notify_size();
}
/// Set whether to inhibit the image of the avatar.
fn set_inhibit_image(&self, inhibit: bool) {
if self.inhibit_image.get() == inhibit {
/// Set the safety setting to watch to decide whether the image of the
/// avatar should be displayed.
fn set_watched_safety_setting(&self, setting: AvatarImageSafetySetting) {
if self.watched_safety_setting.get() == setting {
return;
}
self.inhibit_image.set(inhibit);
self.disconnect_safety_setting_signals();
self.watched_safety_setting.set(setting);
self.connect_safety_setting_signals();
self.obj().notify_watched_safety_setting();
}
/// Set the room to watch to apply the current safety settings.
fn set_watched_room(&self, room: Option<Room>) {
if *self.watched_room.borrow() == room {
return;
}
self.disconnect_safety_setting_signals();
self.watched_room.replace(room);
self.connect_safety_setting_signals();
self.obj().notify_watched_room();
}
/// Connect to the proper signals for the current safety setting.
fn connect_safety_setting_signals(&self) {
let Some(room) = self.watched_room.borrow().clone() else {
return;
};
let Some(session) = room.session() else {
return;
};
match self.watched_safety_setting.get() {
AvatarImageSafetySetting::None => {}
AvatarImageSafetySetting::MediaPreviews => {
let room_handler = room.connect_join_rule_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_paintable();
}
));
self.watched_room_handler.replace(Some(room_handler));
let session_settings_handler = session
.settings()
.connect_media_previews_enabled_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_paintable();
}
));
self.watched_session_settings_handler
.replace(Some(session_settings_handler));
}
AvatarImageSafetySetting::InviteAvatars => {
let room_handler = room.connect_is_invite_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_paintable();
}
));
self.watched_room_handler.replace(Some(room_handler));
let session_settings_handler = session
.settings()
.connect_invite_avatars_enabled_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_paintable();
}
));
self.watched_session_settings_handler
.replace(Some(session_settings_handler));
}
}
self.update_paintable();
self.obj().notify_inhibit_image();
}
/// Disconnect the handlers for the signals of the safety setting.
fn disconnect_safety_setting_signals(&self) {
if let Some(room) = self.watched_room.borrow().as_ref() {
if let Some(handler) = self.watched_room_handler.take() {
room.disconnect(handler);
}
if let Some(handler) = self.watched_session_settings_handler.take() {
room.session()
.inspect(|session| session.settings().disconnect(handler));
}
}
}
/// Whether we can display the image of the avatar with the current
/// state.
fn can_show_image(&self) -> bool {
let watched_safety_setting = self.watched_safety_setting.get();
if watched_safety_setting == AvatarImageSafetySetting::None {
return true;
}
let Some(room) = self.watched_room.borrow().clone() else {
return false;
};
let Some(session) = room.session() else {
return false;
};
match watched_safety_setting {
AvatarImageSafetySetting::None => unreachable!(),
AvatarImageSafetySetting::MediaPreviews => {
session.settings().should_room_show_media_previews(&room)
}
AvatarImageSafetySetting::InviteAvatars => {
!room.is_invite() || session.settings().invite_avatars_enabled()
}
}
}
/// Set the [`AvatarData`] displayed by this widget.
@ -208,7 +360,7 @@ mod imp {
fn update_paintable(&self) {
let _old_paintable_ref = self.paintable_ref.take();
if self.inhibit_image.get() {
if !self.can_show_image() {
// We need to unset the paintable.
self.avatar.set_custom_image(None::<&gdk::Paintable>);
self.update_animated_paintable_state();
@ -245,7 +397,7 @@ mod imp {
fn update_animated_paintable_state(&self) {
let _old_paintable_animation_ref = self.paintable_animation_ref.take();
if self.inhibit_image.get() || !self.obj().is_mapped() {
if !self.can_show_image() || !self.obj().is_mapped() {
// We do not need to animate the paintable.
return;
}

View File

@ -1,18 +1,20 @@
use gettextrs::gettext;
use gtk::{glib, subclass::prelude::*};
use ruma::OwnedRoomId;
use gtk::{glib, prelude::*, subclass::prelude::*};
use ruma::RoomId;
use crate::{components::PillSource, prelude::*};
use crate::{components::PillSource, prelude::*, session::model::Room};
mod imp {
use std::cell::OnceCell;
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::AtRoom)]
pub struct AtRoom {
/// The ID of the room currently represented.
room_id: OnceCell<OwnedRoomId>,
/// The room represented by this mention.
#[property(get, set = Self::set_room, construct_only)]
room: OnceCell<Room>,
}
#[glib::object_subclass]
@ -22,6 +24,7 @@ mod imp {
type ParentType = PillSource;
}
#[glib::derived_properties]
impl ObjectImpl for AtRoom {}
impl PillSourceImpl for AtRoom {
@ -31,14 +34,23 @@ mod imp {
}
impl AtRoom {
/// Set the ID of the room currently represented.
pub(super) fn set_room_id(&self, room_id: OwnedRoomId) {
self.room_id.set(room_id).expect("room ID is uninitialized");
/// Set the room represented by this mention.
fn set_room(&self, room: Room) {
let room = self.room.get_or_init(|| room);
// Bind the avatar image so it always looks the same.
room.avatar_data()
.bind_property("image", &self.obj().avatar_data(), "image")
.sync_create()
.build();
}
/// The ID of the room currently represented.
pub(super) fn room_id(&self) -> &OwnedRoomId {
self.room_id.get().expect("room ID is initialized")
/// The ID of the room represented by this mention.
pub(super) fn room_id(&self) -> &RoomId {
self.room
.get()
.expect("room should be initialized")
.room_id()
}
}
}
@ -49,19 +61,16 @@ glib::wrapper! {
}
impl AtRoom {
/// Constructs an empty `@room` mention.
pub fn new(room_id: OwnedRoomId) -> Self {
let obj = glib::Object::builder::<Self>()
/// Constructs an `@room` mention for the given room.
pub fn new(room: &Room) -> Self {
glib::Object::builder()
.property("display-name", "@room")
.build();
obj.imp().set_room_id(room_id);
obj
.property("room", room)
.build()
}
/// The ID of the room currently represented.
pub fn room_id(&self) -> &OwnedRoomId {
/// The ID of the room represented by this mention.
pub fn room_id(&self) -> &RoomId {
self.imp().room_id()
}
}

View File

@ -12,7 +12,7 @@ pub use self::{
source::{PillSource, PillSourceExt, PillSourceImpl},
source_row::PillSourceRow,
};
use super::{Avatar, RoomPreviewDialog, UserProfileDialog};
use super::{Avatar, AvatarImageSafetySetting, RoomPreviewDialog, UserProfileDialog};
use crate::{
prelude::*,
session::{
@ -48,11 +48,15 @@ mod imp {
/// Whether the pill can be activated.
#[property(get, set = Self::set_activatable, explicit_notify)]
activatable: Cell<bool>,
/// Whether to inhibit the image of the avatar.
/// The safety setting to watch to decide whether the image of the
/// avatar should be displayed.
#[property(get = Self::watched_safety_setting, set = Self::set_watched_safety_setting, builder(AvatarImageSafetySetting::default()))]
watched_safety_setting: PhantomData<AvatarImageSafetySetting>,
/// The room to watch to apply the current safety settings.
///
/// If the image is inhibited, it will not be loaded.
#[property(get = Self::inhibit_image, set = Self::set_inhibit_image)]
inhibit_image: PhantomData<bool>,
/// This is required if `watched_safety_setting` is not `None`.
#[property(get = Self::watched_room, set = Self::set_watched_room, nullable)]
watched_room: PhantomData<Option<Room>>,
gesture_click: RefCell<Option<gtk::GestureClick>>,
}
@ -173,14 +177,26 @@ mod imp {
}
}
/// Whether to inhibit the image of the avatar.
fn inhibit_image(&self) -> bool {
self.avatar.inhibit_image()
/// The safety setting to watch to decide whether the image of the
/// avatar should be displayed.
fn watched_safety_setting(&self) -> AvatarImageSafetySetting {
self.avatar.watched_safety_setting()
}
/// Set whether to inhibit the image of the avatar.
fn set_inhibit_image(&self, inhibit: bool) {
self.avatar.set_inhibit_image(inhibit);
/// Set the safety setting to watch to decide whether the image of the
/// avatar should be displayed.
fn set_watched_safety_setting(&self, setting: AvatarImageSafetySetting) {
self.avatar.set_watched_safety_setting(setting);
}
/// The room to watch to apply the current safety settings.
fn watched_room(&self) -> Option<Room> {
self.avatar.watched_room()
}
/// Set the room to watch to apply the current safety settings.
fn set_watched_room(&self, room: Option<Room>) {
self.avatar.set_watched_room(room);
}
/// Set the display name of this pill.
@ -240,8 +256,30 @@ glib::wrapper! {
}
impl Pill {
/// Create a pill with the given source.
pub fn new(source: &impl IsA<PillSource>) -> Self {
glib::Object::builder().property("source", source).build()
/// Create a pill with the given source and watching the given safety
/// setting.
pub fn new(
source: &impl IsA<PillSource>,
watched_safety_setting: AvatarImageSafetySetting,
watched_room: Option<Room>,
) -> Self {
let source = source.upcast_ref();
let (watched_safety_setting, watched_room) = if let Some(room) = source
.downcast_ref::<Room>()
.cloned()
.or_else(|| source.downcast_ref::<AtRoom>().map(AtRoom::room))
{
// We must always watch the invite avatars setting for local rooms.
(AvatarImageSafetySetting::InviteAvatars, Some(room))
} else {
(watched_safety_setting, watched_room)
};
glib::Object::builder()
.property("source", source)
.property("watched-safety-setting", watched_safety_setting)
.property("watched-room", watched_room)
.build()
}
}

View File

@ -5,7 +5,7 @@ use gtk::{
CompositeTemplate,
};
use crate::components::{Pill, PillSource};
use crate::components::{AvatarImageSafetySetting, Pill, PillSource};
mod imp {
use std::{cell::RefCell, collections::HashMap, marker::PhantomData, sync::LazyLock};
@ -175,7 +175,9 @@ mod imp {
return;
}
let pill = Pill::new(source);
// We do not need to watch the safety setting as this entry should only be used
// with search results.
let pill = Pill::new(source, AvatarImageSafetySetting::None, None);
pill.set_margin_start(3);
pill.set_margin_end(3);

View File

@ -1,7 +1,10 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use super::Pill;
use crate::components::AvatarData;
use crate::{
components::{AvatarData, AvatarImageSafetySetting},
session::model::Room,
};
mod imp {
use std::{cell::Cell, marker::PhantomData};
@ -153,8 +156,13 @@ pub trait PillSourceExt: 'static {
f: F,
) -> glib::SignalHandlerId;
/// Get a `Pill` representing this source.
fn to_pill(&self) -> Pill;
/// Get a `Pill` representing this source, watching the given safety
/// setting.
fn to_pill(
&self,
watched_safety_setting: AvatarImageSafetySetting,
watched_room: Option<Room>,
) -> Pill;
}
impl<O: IsA<PillSource>> PillSourceExt for O {
@ -209,8 +217,12 @@ impl<O: IsA<PillSource>> PillSourceExt for O {
}
/// Get a `Pill` representing this source.
fn to_pill(&self) -> Pill {
Pill::new(self)
fn to_pill(
&self,
watched_safety_setting: AvatarImageSafetySetting,
watched_room: Option<Room>,
) -> Pill {
Pill::new(self, watched_safety_setting, watched_room)
}
}

View File

@ -1,6 +1,7 @@
use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use super::{Avatar, PillSource};
use super::{AtRoom, Avatar, AvatarImageSafetySetting, PillSource};
use crate::session::model::Room;
mod imp {
use std::cell::RefCell;
@ -52,6 +53,20 @@ mod imp {
return;
}
let (watched_safety_setting, watched_room) = if let Some(room) = source
.and_downcast_ref::<Room>()
.cloned()
.or_else(|| source.and_downcast_ref::<AtRoom>().map(AtRoom::room))
{
// We must always watch the invite avatars setting for local rooms.
(AvatarImageSafetySetting::InviteAvatars, Some(room))
} else {
(AvatarImageSafetySetting::None, None)
};
self.avatar
.set_watched_safety_setting(watched_safety_setting);
self.avatar.set_watched_room(watched_room);
self.source.replace(source);
self.obj().notify_source();
}

View File

@ -1660,15 +1660,7 @@ impl Room {
/// Constructs an `AtRoom` for this room.
pub(crate) fn at_room(&self) -> AtRoom {
let at_room = AtRoom::new(self.room_id().to_owned());
// Bind the avatar image so it always looks the same.
self.avatar_data()
.bind_property("image", &at_room.avatar_data(), "image")
.sync_create()
.build();
at_room
AtRoom::new(self)
}
/// Get or create the list of members of this room.

View File

@ -3,7 +3,10 @@ use gettextrs::gettext;
use gtk::{glib, glib::clone, prelude::*, CompositeTemplate};
use crate::{
components::{confirm_leave_room_dialog, Avatar, LabelWithWidgets, LoadingButton, Pill},
components::{
confirm_leave_room_dialog, Avatar, AvatarImageSafetySetting, LabelWithWidgets,
LoadingButton, Pill,
},
gettext_f,
prelude::*,
session::model::{MemberList, Room, RoomCategory, TargetRoomCategory, User},
@ -22,29 +25,30 @@ mod imp {
#[template(resource = "/org/gnome/Fractal/ui/session/view/content/invite.ui")]
#[properties(wrapper_type = super::Invite)]
pub struct Invite {
/// The room currently displayed.
#[property(get, set = Self::set_room, explicit_notify, nullable)]
pub room: RefCell<Option<Room>>,
pub room_members: RefCell<Option<MemberList>>,
pub accept_requests: RefCell<HashSet<Room>>,
pub decline_requests: RefCell<HashSet<Room>>,
category_handler: RefCell<Option<glib::SignalHandlerId>>,
invite_avatars_handler: RefCell<Option<glib::SignalHandlerId>>,
inviter_pill: RefCell<Option<Pill>>,
#[template_child]
pub header_bar: TemplateChild<adw::HeaderBar>,
pub(super) header_bar: TemplateChild<adw::HeaderBar>,
#[template_child]
avatar: TemplateChild<Avatar>,
#[template_child]
pub room_alias: TemplateChild<gtk::Label>,
room_alias: TemplateChild<gtk::Label>,
#[template_child]
pub room_topic: TemplateChild<gtk::Label>,
room_topic: TemplateChild<gtk::Label>,
#[template_child]
pub inviter: TemplateChild<LabelWithWidgets>,
inviter: TemplateChild<LabelWithWidgets>,
#[template_child]
pub accept_button: TemplateChild<LoadingButton>,
accept_button: TemplateChild<LoadingButton>,
#[template_child]
pub decline_button: TemplateChild<LoadingButton>,
decline_button: TemplateChild<LoadingButton>,
/// The room currently displayed.
#[property(get, set = Self::set_room, explicit_notify, nullable)]
room: RefCell<Option<Room>>,
/// The list of members in the room.
room_members: RefCell<Option<MemberList>>,
/// The rooms that are currently being accepted.
accept_requests: RefCell<HashSet<Room>>,
/// The rooms that are currently being declined.
decline_requests: RefCell<HashSet<Room>>,
category_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
@ -55,15 +59,9 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::bind_template_callbacks(klass);
klass.set_accessible_role(gtk::AccessibleRole::Group);
klass.install_action_async("invite.decline", None, move |widget, _, _| async move {
widget.decline().await;
});
klass.install_action_async("invite.accept", None, move |widget, _, _| async move {
widget.accept().await;
});
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -118,6 +116,7 @@ mod imp {
impl BinImpl for Invite {}
#[gtk::template_callbacks]
impl Invite {
/// Set the room currently displayed.
fn set_room(&self, room: Option<Room>) {
@ -126,28 +125,25 @@ mod imp {
}
self.disconnect_signals();
self.inviter_pill.take();
let obj = self.obj();
match &room {
Some(room) if self.accept_requests.borrow().contains(room) => {
obj.action_set_enabled("invite.accept", false);
obj.action_set_enabled("invite.decline", false);
self.decline_button.set_is_loading(false);
self.decline_button.set_sensitive(false);
self.accept_button.set_is_loading(true);
}
Some(room) if self.decline_requests.borrow().contains(room) => {
obj.action_set_enabled("invite.accept", false);
obj.action_set_enabled("invite.decline", false);
self.accept_button.set_is_loading(false);
self.accept_button.set_sensitive(false);
self.decline_button.set_is_loading(true);
}
_ => obj.reset(),
_ => self.reset(),
}
if let Some(room) = &room {
let category_handler = room.connect_category_notify(clone!(
#[weak]
obj,
#[weak(rename_to = imp)]
self,
move |room| {
let category = room.category();
@ -168,10 +164,10 @@ mod imp {
}
if category != RoomCategory::Invited {
let imp = obj.imp();
imp.decline_requests.borrow_mut().remove(room);
imp.accept_requests.borrow_mut().remove(room);
obj.reset();
imp.reset();
if let Some(category_handler) = imp.category_handler.take() {
room.disconnect(category_handler);
}
@ -180,31 +176,12 @@ mod imp {
));
self.category_handler.replace(Some(category_handler));
if let Some(session) = room.session() {
let settings = session.settings();
let invite_avatars_handler =
settings.connect_invite_avatars_enabled_notify(clone!(
#[weak(rename_to = imp)]
self,
move |settings| {
let inhibit_images = !settings.invite_avatars_enabled();
imp.avatar.set_inhibit_image(inhibit_images);
if let Some(pill) = &*imp.inviter_pill.borrow() {
pill.set_inhibit_image(inhibit_images);
};
}
));
self.invite_avatars_handler
.replace(Some(invite_avatars_handler));
let inhibit_images = !settings.invite_avatars_enabled();
self.avatar.set_inhibit_image(inhibit_images);
if let Some(inviter) = room.inviter() {
let pill = inviter.to_pill();
pill.set_inhibit_image(inhibit_images);
let pill = Pill::new(
&inviter,
AvatarImageSafetySetting::InviteAvatars,
Some(room.clone()),
);
let label = gettext_f(
// Translators: Do NOT translate the content between '{' and '}', these
@ -218,8 +195,6 @@ mod imp {
self.inviter
.set_label_and_widgets(label, vec![pill.clone()]);
self.inviter_pill.replace(Some(pill));
}
}
}
@ -228,7 +203,96 @@ mod imp {
.replace(room.as_ref().map(Room::get_or_create_members));
self.room.replace(room);
obj.notify_room();
self.obj().notify_room();
}
/// Reset the state of the view.
fn reset(&self) {
self.accept_button.set_is_loading(false);
self.accept_button.set_sensitive(true);
self.decline_button.set_is_loading(false);
self.decline_button.set_sensitive(true);
}
/// Accept the invite.
#[template_callback]
async fn accept(&self) {
let Some(room) = self.room.borrow().clone() else {
return;
};
self.decline_button.set_sensitive(false);
self.accept_button.set_is_loading(true);
self.accept_requests.borrow_mut().insert(room.clone());
if room
.change_category(TargetRoomCategory::Normal)
.await
.is_err()
{
toast!(
self.obj(),
gettext(
// Translators: Do NOT translate the content between '{' and '}', this
// is a variable name.
"Could not accept invitation for {room}",
),
@room,
);
self.accept_requests.borrow_mut().remove(&room);
self.reset();
}
}
/// Decline the invite.
#[template_callback]
async fn decline(&self) {
let Some(room) = self.room.borrow().clone() else {
return;
};
let obj = self.obj();
let Some(response) = confirm_leave_room_dialog(&room, &*obj).await else {
return;
};
self.accept_button.set_sensitive(false);
self.decline_button.set_is_loading(true);
self.decline_requests.borrow_mut().insert(room.clone());
let ignored_inviter = response.ignore_inviter.then(|| room.inviter()).flatten();
let closed = if room.change_category(TargetRoomCategory::Left).await.is_ok() {
// A room where we were invited is usually empty so just close it.
let _ = obj.activate_action("session.close-room", None);
true
} else {
toast!(
obj,
gettext(
// Translators: Do NOT translate the content between '{' and '}', this
// is a variable name.
"Could not decline invitation for {room}",
),
@room,
);
self.decline_requests.borrow_mut().remove(&room);
self.reset();
false
};
if let Some(inviter) = ignored_inviter {
if inviter.upcast::<User>().ignore().await.is_err() {
toast!(obj, gettext("Could not ignore user"));
} else if !closed {
// Ignoring the user should remove the room from the sidebar so close it.
let _ = obj.activate_action("session.close-room", None);
}
}
}
/// Disconnect the signal handlers of this view.
@ -237,12 +301,6 @@ mod imp {
if let Some(handler) = self.category_handler.take() {
room.disconnect(handler);
}
if let Some(session) = room.session() {
if let Some(handler) = self.invite_avatars_handler.take() {
session.settings().disconnect(handler);
}
}
}
}
}
@ -263,92 +321,4 @@ impl Invite {
pub fn header_bar(&self) -> &adw::HeaderBar {
&self.imp().header_bar
}
fn reset(&self) {
let imp = self.imp();
imp.accept_button.set_is_loading(false);
imp.decline_button.set_is_loading(false);
self.action_set_enabled("invite.accept", true);
self.action_set_enabled("invite.decline", true);
}
/// Accept the invite.
async fn accept(&self) {
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
self.action_set_enabled("invite.accept", false);
self.action_set_enabled("invite.decline", false);
imp.accept_button.set_is_loading(true);
imp.accept_requests.borrow_mut().insert(room.clone());
if room
.change_category(TargetRoomCategory::Normal)
.await
.is_err()
{
toast!(
self,
gettext(
// Translators: Do NOT translate the content between '{' and '}', this
// is a variable name.
"Could not accept invitation for {room}",
),
@room,
);
imp.accept_requests.borrow_mut().remove(&room);
self.reset();
}
}
/// Decline the invite.
async fn decline(&self) {
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
let Some(response) = confirm_leave_room_dialog(&room, self).await else {
return;
};
self.action_set_enabled("invite.accept", false);
self.action_set_enabled("invite.decline", false);
imp.decline_button.set_is_loading(true);
imp.decline_requests.borrow_mut().insert(room.clone());
let ignored_inviter = response.ignore_inviter.then(|| room.inviter()).flatten();
let closed = if room.change_category(TargetRoomCategory::Left).await.is_ok() {
// A room where we were invited is usually empty so just close it.
let _ = self.activate_action("session.close-room", None);
true
} else {
toast!(
self,
gettext(
// Translators: Do NOT translate the content between '{' and '}', this
// is a variable name.
"Could not decline invitation for {room}",
),
@room,
);
imp.decline_requests.borrow_mut().remove(&room);
self.reset();
false
};
if let Some(inviter) = ignored_inviter {
if inviter.upcast::<User>().ignore().await.is_err() {
toast!(self, gettext("Could not ignore user"));
} else if !closed {
// Ignoring the user should remove the room from the sidebar so close it.
let _ = self.activate_action("session.close-room", None);
}
}
}
}

View File

@ -46,6 +46,10 @@
<lookup name="room">ContentInvite</lookup>
</lookup>
</binding>
<binding name="watched-room">
<lookup name="room">ContentInvite</lookup>
</binding>
<property name="watched-safety-setting">invite-avatars</property>
<property name="accessible-role">presentation</property>
</object>
</child>
@ -114,7 +118,7 @@
<child>
<object class="LoadingButton" id="decline_button">
<property name="content-label" translatable="yes">_Decline</property>
<property name="action-name">invite.decline</property>
<signal name="clicked" handler="decline" swapped="yes"/>
<style>
<class name="pill"/>
<class name="large"/>
@ -124,7 +128,7 @@
<child>
<object class="LoadingButton" id="accept_button">
<property name="content-label" translatable="yes">_Accept</property>
<property name="action-name">invite.accept</property>
<signal name="clicked" handler="accept" swapped="yes"/>
<style>
<class name="suggested-action"/>
<class name="pill"/>

View File

@ -118,7 +118,6 @@ mod imp {
#[property(get)]
is_published: Cell<bool>,
expr_watch: RefCell<Option<gtk::ExpressionWatch>>,
invite_avatars_handler: RefCell<Option<glib::SignalHandlerId>>,
notifications_settings_handlers: RefCell<Vec<glib::SignalHandlerId>>,
membership_handler: RefCell<Option<glib::SignalHandlerId>>,
permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
@ -313,31 +312,12 @@ mod imp {
imp.update_encryption();
}
)),
room.connect_is_invite_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_image();
}
)),
];
self.room.set(room, room_handler_ids);
obj.notify_room();
if let Some(session) = room.session() {
let invite_avatars_handler = session
.settings()
.connect_invite_avatars_enabled_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_image();
}
));
self.invite_avatars_handler
.replace(Some(invite_avatars_handler));
let notifications_settings = session.notifications().settings();
let notifications_settings_handlers = vec![
notifications_settings.connect_account_enabled_notify(clone!(
@ -360,7 +340,6 @@ mod imp {
.replace(notifications_settings_handlers);
}
self.update_image();
self.init_edit_details();
self.update_members();
self.update_notifications();
@ -436,20 +415,6 @@ mod imp {
);
}
/// Update the image of the avatar of the room according to the current
/// state.
fn update_image(&self) {
let Some(room) = self.room.obj() else {
return;
};
let Some(session) = room.session() else {
return;
};
let inhibit_image = room.is_invite() && !session.settings().invite_avatars_enabled();
self.avatar.set_inhibit_image(inhibit_image);
}
/// Initialize the button to edit details.
fn init_edit_details(&self) {
let Some(room) = self.room.obj() else {
@ -560,10 +525,6 @@ mod imp {
fn disconnect_all(&self) {
if let Some(room) = self.room.obj() {
if let Some(session) = room.session() {
if let Some(handler) = self.invite_avatars_handler.take() {
session.settings().disconnect(handler);
}
for handler in self.notifications_settings_handlers.take() {
session.notifications().settings().disconnect(handler);
}

View File

@ -17,6 +17,10 @@
<lookup name="room">RoomDetailsGeneralPage</lookup>
</lookup>
</binding>
<binding name="watched-room">
<lookup name="room">RoomDetailsGeneralPage</lookup>
</binding>
<property name="watched-safety-setting">invite-avatars</property>
</object>
</child>
<child>

View File

@ -5,7 +5,7 @@ use secular::normalized_lower_lay_string;
use super::{CompletionMemberList, CompletionRoomList};
use crate::{
components::{Pill, PillSource, PillSourceRow},
components::{AvatarImageSafetySetting, Pill, PillSource, PillSourceRow},
session::{model::Room, view::content::room_history::message_toolbar::MessageToolbar},
utils::BoundObject,
};
@ -651,7 +651,9 @@ mod imp {
buffer.delete(&mut start, &mut end);
let pill = Pill::new(&source);
// We do not need to watch safety settings for mentions, rooms will be watched
// automatically.
let pill = Pill::new(&source, AvatarImageSafetySetting::None, None);
self.message_toolbar()
.current_composer_state()
.add_widget(pill, &mut start);

View File

@ -18,8 +18,7 @@ use tracing::{error, warn};
use super::ComposerParser;
use crate::{
components::{Pill, PillSource},
prelude::*,
components::{AvatarImageSafetySetting, Pill, PillSource},
session::model::{Event, Member, Room, Timeline},
spawn, spawn_tokio,
utils::matrix::{find_at_room, find_html_mentions, AT_ROOM},
@ -312,7 +311,10 @@ mod imp {
match DraftMention::new(&room, &text[content_start..content_end]) {
DraftMention::Source(source) => {
self.add_widget(source.to_pill().upcast(), &mut end_iter);
// We do not need to watch safety settings for mentions, rooms will be
// watched automatically.
let pill = Pill::new(&source, AvatarImageSafetySetting::None, None);
self.add_widget(pill.upcast(), &mut end_iter);
}
DraftMention::Text(s) => {
self.buffer.insert(&mut end_iter, s);
@ -387,7 +389,9 @@ mod imp {
let can_contain_at_room = message.mentions().is_none_or(|m| m.room);
if room.permissions().can_notify_room() && can_contain_at_room {
if let Some(start) = find_at_room(&text) {
let pill = room.at_room().to_pill();
// We do not need to watch safety settings for at-room mentions, our own member
// is in the room.
let pill = Pill::new(&room.at_room(), AvatarImageSafetySetting::None, None);
let end = start + AT_ROOM.len();
mentions.push(DetectedMention { pill, start, end });

View File

@ -37,7 +37,7 @@ use self::{
};
use super::message_row::MessageContent;
use crate::{
components::{CustomEntry, LabelWithWidgets, LoadingButton},
components::{AvatarImageSafetySetting, CustomEntry, LabelWithWidgets, LoadingButton},
gettext_f,
prelude::*,
session::model::{Event, Member, Room, RoomListRoomInfo, Timeline},
@ -516,7 +516,9 @@ mod imp {
"Reply to {user}",
&[("user", LabelWithWidgets::PLACEHOLDER)],
);
let pill = sender.to_pill();
// We do not need to watch safety settings for mentions, rooms will be watched
// automatically.
let pill = sender.to_pill(AvatarImageSafetySetting::None, None);
self.related_event_header
.set_label_and_widgets(label, vec![pill]);
@ -551,7 +553,8 @@ mod imp {
let buffer = self.message_entry.buffer();
let mut insert = buffer.iter_at_mark(&buffer.get_insert());
let pill = member.to_pill();
// We do not need to watch safety settings for users.
let pill = member.to_pill(AvatarImageSafetySetting::None, None);
self.current_composer_state().add_widget(pill, &mut insert);
self.message_entry.grab_focus();

View File

@ -33,7 +33,6 @@ mod imp {
#[template_child]
notification_count: TemplateChild<gtk::Label>,
direct_icon: RefCell<Option<gtk::Image>>,
invite_avatars_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
@ -86,10 +85,6 @@ mod imp {
));
self.obj().add_controller(drag);
}
fn dispose(&self) {
self.disconnect_signals();
}
}
impl WidgetImpl for SidebarRoomRow {}
@ -102,23 +97,9 @@ mod imp {
return;
}
self.disconnect_signals();
self.room.disconnect_signals();
if let Some(room) = room {
if let Some(session) = room.session() {
let invite_avatars_handler = session
.settings()
.connect_invite_avatars_enabled_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_image();
}
));
self.invite_avatars_handler
.replace(Some(invite_avatars_handler));
}
let highlight_handler = room.connect_highlight_notify(clone!(
#[weak(rename_to = imp)]
self,
@ -154,13 +135,6 @@ mod imp {
imp.update_display_name();
}
));
let is_invite_handler = room.connect_is_invite_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_image();
}
));
self.room.set(
room,
@ -170,34 +144,18 @@ mod imp {
name_handler,
notifications_count_handler,
category_handler,
is_invite_handler,
],
);
self.update_accessibility_label();
}
self.update_image();
self.update_display_name();
self.update_highlight();
self.update_direct_icon();
self.obj().notify_room();
}
/// Update the image of the avatar of the room according to the current
/// state.
fn update_image(&self) {
let Some(room) = self.room.obj() else {
return;
};
let Some(session) = room.session() else {
return;
};
let inhibit_image = room.is_invite() && !session.settings().invite_avatars_enabled();
self.avatar.set_inhibit_image(inhibit_image);
}
/// Update the display name of the room according to the current state.
fn update_display_name(&self) {
let Some(room) = self.room.obj() else {
@ -346,17 +304,6 @@ mod imp {
name
}
}
/// Disconnect the signal handlers of this row.
fn disconnect_signals(&self) {
if let Some(session) = self.room.obj().and_then(|room| room.session()) {
if let Some(handler) = self.invite_avatars_handler.take() {
session.settings().disconnect(handler);
}
}
self.room.disconnect_signals();
}
}
}

View File

@ -17,6 +17,10 @@
<lookup name="room">SidebarRoomRow</lookup>
</lookup>
</binding>
<binding name="watched-room">
<lookup name="room">SidebarRoomRow</lookup>
</binding>
<property name="watched-safety-setting">invite-avatars</property>
<property name="accessible-role">presentation</property>
</object>
</child>

View File

@ -33,7 +33,12 @@ pub(crate) mod ext_traits;
mod media_message;
pub(crate) use self::media_message::*;
use crate::{components::Pill, prelude::*, secret::StoredSession, session::model::Room};
use crate::{
components::{AvatarImageSafetySetting, Pill},
prelude::*,
secret::StoredSession,
session::model::Room,
};
/// The result of a password validation.
#[derive(Debug, Default, Clone, Copy)]
@ -382,18 +387,29 @@ impl MatrixIdUri {
match self {
Self::Room(room_uri) => {
let session = room.session()?;
session
.room_list()
.get_by_identifier(&room_uri.id)
.as_ref()
.map(Pill::new)
.or_else(|| Some(Pill::new(&session.remote_cache().room(room_uri))))
let pill =
if let Some(uri_room) = session.room_list().get_by_identifier(&room_uri.id) {
// We do not need to watch safety settings for local rooms, they will be
// watched automatically.
Pill::new(&uri_room, AvatarImageSafetySetting::None, None)
} else {
Pill::new(
&session.remote_cache().room(room_uri),
AvatarImageSafetySetting::MediaPreviews,
Some(room.clone()),
)
};
Some(pill)
}
Self::User(user_id) => {
// We should have a strong reference to the list wherever we show a user pill,
// so we can use `get_or_create_members()`.
let user = room.get_or_create_members().get_or_create(user_id);
Some(Pill::new(&user))
// We do not need to watch safety settings for users.
Some(Pill::new(&user, AvatarImageSafetySetting::None, None))
}
Self::Event(_) => None,
}

View File

@ -12,7 +12,7 @@ mod tests;
use super::matrix::{find_at_room, MatrixIdUri, AT_ROOM};
use crate::{
components::{LabelWithWidgets, Pill},
components::{AvatarImageSafetySetting, LabelWithWidgets, Pill},
prelude::*,
session::model::Room,
};
@ -182,7 +182,9 @@ impl PangoStrMutExt for String {
self.push_str(LabelWithWidgets::PLACEHOLDER);
self.push_str(&(&s[pos + AT_ROOM.len()..]).escape_markup());
Some(room.at_room().to_pill())
// We do not need to watch safety settings for mentions, rooms will be watched
// automatically.
Some(room.at_room().to_pill(AvatarImageSafetySetting::None, None))
} else {
self.push_str(&s.escape_markup());
None

View File

@ -128,7 +128,9 @@ macro_rules! _toast_accum {
([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident = $val:expr, $($tail:tt)*) => {
{
use $crate::components::PillSourceExt;
let pill: $crate::components::Pill = $val.to_pill();
// We do not need to watch safety settings for pills, rooms will be watched
// automatically.
let pill: $crate::components::Pill = $val.to_pill($crate::components::AvatarImageSafetySetting::None, None);
$crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*)
}
};
@ -136,7 +138,9 @@ macro_rules! _toast_accum {
([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident $($tail:tt)*) => {
{
use $crate::components::PillSourceExt;
let pill: $crate::components::Pill = $var.to_pill();
// We do not need to watch safety settings for pills, rooms will be watched
// automatically.
let pill: $crate::components::Pill = $var.to_pill($crate::components::AvatarImageSafetySetting::None, None);
$crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),] $($tail)*)
}
};