Browse Source

room-details: Edit history visibility in a subpage

The strings for the choices are too long, they could be ellipsized in
a popover. So we use a new subpage instead, where the strings can be as
long as they need to be.

This removes ComboLoadingRow since it is now unused.
fractal-13
Kévin Commaille 7 months ago committed by Kévin Commaille
parent
commit
2f710d149d
  1. 13
      data/resources/stylesheet/_common.scss
  2. 2
      po/POTFILES.in
  3. 47
      src/components/rows/combo_loading_row.blp
  4. 278
      src/components/rows/combo_loading_row.rs
  5. 7
      src/components/rows/mod.rs
  6. 18
      src/session/view/content/room_details/general_page.blp
  7. 70
      src/session/view/content/room_details/general_page.rs
  8. 79
      src/session/view/content/room_details/history_visibility_subpage.blp
  9. 241
      src/session/view/content/room_details/history_visibility_subpage.rs
  10. 5
      src/session/view/content/room_details/mod.rs
  11. 2
      src/ui-blueprint-resources.in

13
data/resources/stylesheet/_common.scss

@ -32,9 +32,14 @@ headerbar .suggested-action, .standalone-button {
}
.form-page {
scrolledwindow > viewport > clamp > box {
margin: 42px 12px;
border-spacing: 24px;
scrolledwindow > viewport > clamp {
> * {
margin: 42px 12px;
}
> box {
border-spacing: 24px;
}
}
levelbar.discrete block {
@ -77,7 +82,7 @@ button.overlaid {
}
.avatar-row-list {
row {
row {
&:first-child {
margin-top: 0px;
}

2
po/POTFILES.in

@ -131,6 +131,8 @@ src/session/view/content/room_details/history_viewer/file_row.rs
src/session/view/content/room_details/history_viewer/file_row.blp
src/session/view/content/room_details/history_viewer/file.blp
src/session/view/content/room_details/history_viewer/visual_media.blp
src/session/view/content/room_details/history_visibility_subpage.blp
src/session/view/content/room_details/history_visibility_subpage.rs
src/session/view/content/room_details/invite_subpage/list.rs
src/session/view/content/room_details/invite_subpage/mod.rs
src/session/view/content/room_details/invite_subpage/mod.blp

47
src/components/rows/combo_loading_row.blp

@ -1,47 +0,0 @@
using Gtk 4.0;
using Adw 1;
template $ComboLoadingRow: Adw.ActionRow {
selectable: false;
activatable: bind template.read-only inverted;
styles [
"combo",
"property",
]
Gtk.Box arrow_box {
valign: center;
$LoadingBin loading_bin {
child: Gtk.Image {
visible: bind template.read-only inverted;
icon-name: "pan-down-symbolic";
accessible-role: presentation;
styles [
"dropdown-arrow",
]
};
}
Gtk.Popover popover {
notify::visible => $popover_visible() swapped;
styles [
"menu",
]
child: Gtk.ScrolledWindow {
hscrollbar-policy: never;
max-content-height: 400;
propagate-natural-width: true;
propagate-natural-height: true;
child: Gtk.ListBox list {
row-activated => $row_activated() swapped;
};
};
}
}
}

278
src/components/rows/combo_loading_row.rs

@ -1,278 +0,0 @@
use adw::{prelude::*, subclass::prelude::*};
use gtk::{CompositeTemplate, glib, glib::clone, pango};
use crate::{components::LoadingBin, utils::BoundObject};
mod imp {
use std::{
cell::{Cell, RefCell},
marker::PhantomData,
};
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/components/rows/combo_loading_row.ui")]
#[properties(wrapper_type = super::ComboLoadingRow)]
pub struct ComboLoadingRow {
#[template_child]
loading_bin: TemplateChild<LoadingBin>,
#[template_child]
popover: TemplateChild<gtk::Popover>,
#[template_child]
list: TemplateChild<gtk::ListBox>,
/// The string model to build the list.
#[property(get, set = Self::set_string_model, explicit_notify, nullable)]
string_model: BoundObject<gtk::StringList>,
/// The position of the selected string.
#[property(get, default = gtk::INVALID_LIST_POSITION)]
selected: Cell<u32>,
/// The selected string.
#[property(get, set = Self::set_selected_string, explicit_notify, nullable)]
selected_string: RefCell<Option<String>>,
/// Whether the row is loading.
#[property(get = Self::is_loading, set = Self::set_is_loading)]
is_loading: PhantomData<bool>,
/// Whether the row is read-only.
#[property(get, set = Self::set_read_only, explicit_notify)]
read_only: Cell<bool>,
selected_handlers: RefCell<Vec<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for ComboLoadingRow {
const NAME: &'static str = "ComboLoadingRow";
type Type = super::ComboLoadingRow;
type ParentType = adw::ActionRow;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::bind_template_callbacks(klass);
klass.set_accessible_role(gtk::AccessibleRole::ComboBox);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for ComboLoadingRow {}
impl WidgetImpl for ComboLoadingRow {}
impl ListBoxRowImpl for ComboLoadingRow {}
impl PreferencesRowImpl for ComboLoadingRow {}
impl ActionRowImpl for ComboLoadingRow {
fn activate(&self) {
if !self.is_loading() {
self.popover.popup();
}
}
}
#[gtk::template_callbacks]
impl ComboLoadingRow {
/// Set the string model to build the list.
fn set_string_model(&self, model: Option<gtk::StringList>) {
if self.string_model.obj() == model {
return;
}
let obj = self.obj();
for handler in self.selected_handlers.take() {
obj.disconnect(handler);
}
self.string_model.disconnect_signals();
self.list.bind_model(
model.as_ref(),
clone!(
#[weak]
obj,
#[upgrade_or_else]
|| { gtk::ListBoxRow::new().upcast() },
move |item| {
let Some(item) = item.downcast_ref::<gtk::StringObject>() else {
return gtk::ListBoxRow::new().upcast();
};
let string = item.string();
let child = gtk::Box::new(gtk::Orientation::Horizontal, 6);
let label = gtk::Label::builder()
.xalign(0.0)
.ellipsize(pango::EllipsizeMode::End)
.max_width_chars(40)
.valign(gtk::Align::Center)
.label(string)
.build();
child.append(&label);
let icon = gtk::Image::builder()
.accessible_role(gtk::AccessibleRole::Presentation)
.icon_name("object-select-symbolic")
.build();
let selected_handler = obj.connect_selected_string_notify(clone!(
#[weak]
label,
#[weak]
icon,
move |obj| {
let is_selected =
obj.selected_string().is_some_and(|s| s == label.label());
let opacity = if is_selected { 1.0 } else { 0.0 };
icon.set_opacity(opacity);
}
));
obj.imp()
.selected_handlers
.borrow_mut()
.push(selected_handler);
let is_selected = obj.selected_string().is_some_and(|s| s == label.label());
let opacity = if is_selected { 1.0 } else { 0.0 };
icon.set_opacity(opacity);
child.append(&icon);
gtk::ListBoxRow::builder().child(&child).build().upcast()
}
),
);
if let Some(model) = model {
let items_changed_handler = model.connect_items_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_, _, _, _| {
imp.update_selected();
}
));
self.string_model.set(model, vec![items_changed_handler]);
}
self.update_selected();
obj.notify_string_model();
}
/// Set whether the row is loading.
fn set_selected_string(&self, string: Option<String>) {
if *self.selected_string.borrow() == string {
return;
}
let obj = self.obj();
obj.set_subtitle(string.as_deref().unwrap_or_default());
self.selected_string.replace(string);
self.update_selected();
obj.notify_selected_string();
}
/// Update the position of the selected string.
fn update_selected(&self) {
let mut selected = gtk::INVALID_LIST_POSITION;
if let Some((string_model, selected_string)) = self
.string_model
.obj()
.zip(self.selected_string.borrow().clone())
{
for (pos, item) in string_model.iter::<glib::Object>().enumerate() {
let Some(item) = item.ok().and_downcast::<gtk::StringObject>() else {
// The iterator is broken.
break;
};
if item.string() == selected_string {
selected = pos as u32;
break;
}
}
}
if self.selected.get() == selected {
return;
}
self.selected.set(selected);
self.obj().notify_selected();
}
/// Whether the row is loading.
fn is_loading(&self) -> bool {
self.loading_bin.is_loading()
}
/// Set whether the row is loading.
fn set_is_loading(&self, loading: bool) {
if self.is_loading() == loading {
return;
}
self.loading_bin.set_is_loading(loading);
self.obj().notify_is_loading();
}
/// Set whether the row is read-only.
fn set_read_only(&self, read_only: bool) {
if self.read_only.get() == read_only {
return;
}
let obj = self.obj();
self.read_only.set(read_only);
obj.update_property(&[gtk::accessible::Property::ReadOnly(read_only)]);
obj.notify_read_only();
}
/// A row was activated.
#[template_callback]
fn row_activated(&self, row: &gtk::ListBoxRow) {
let Some(string) = row
.child()
.and_downcast::<gtk::Box>()
.and_then(|b| b.first_child())
.and_downcast::<gtk::Label>()
.map(|l| l.label())
else {
return;
};
self.popover.popdown();
self.set_selected_string(Some(string.into()));
}
/// The popover's visibility changed.
#[template_callback]
fn popover_visible(&self) {
let obj = self.obj();
let is_visible = self.popover.is_visible();
if is_visible {
obj.add_css_class("has-open-popup");
} else {
obj.remove_css_class("has-open-popup");
}
}
}
}
glib::wrapper! {
/// An `AdwActionRow` behaving like a combo box, with a loading state.
pub struct ComboLoadingRow(ObjectSubclass<imp::ComboLoadingRow>)
@extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow,
@implements gtk::Actionable, gtk::Accessible;
}
impl ComboLoadingRow {
pub fn new() -> Self {
glib::Object::new()
}
}

7
src/components/rows/mod.rs

@ -2,7 +2,6 @@
mod button_count_row;
mod check_loading_row;
mod combo_loading_row;
mod copyable_row;
mod entry_add_row;
mod loading_button_row;
@ -13,7 +12,7 @@ mod switch_loading_row;
pub use self::{
button_count_row::ButtonCountRow, check_loading_row::CheckLoadingRow,
combo_loading_row::ComboLoadingRow, copyable_row::CopyableRow, entry_add_row::EntryAddRow,
loading_button_row::LoadingButtonRow, loading_row::LoadingRow, removable_row::RemovableRow,
substring_entry_row::SubstringEntryRow, switch_loading_row::SwitchLoadingRow,
copyable_row::CopyableRow, entry_add_row::EntryAddRow, loading_button_row::LoadingButtonRow,
loading_row::LoadingRow, removable_row::RemovableRow, substring_entry_row::SubstringEntryRow,
switch_loading_row::SwitchLoadingRow,
};

18
src/session/view/content/room_details/general_page.blp

@ -214,18 +214,14 @@ template $RoomDetailsGeneralPage: Adw.PreferencesPage {
notify::is-active => $toggle_publish() swapped;
}
$ComboLoadingRow history_visibility {
$ButtonCountRow history_visibility {
title: _("Who Can Read History");
notify::selected-string => $set_history_visibility() swapped;
string-model: Gtk.StringList {
strings [
_("Anyone, even if they are not in the room"),
_("Members only, since this option was selected"),
_("Members only, since they joined the room"),
_("Members only, since they were invited"),
]
};
action-name: "details.show-subpage";
action-target: "'history-visibility'";
styles [
"property",
]
}
}

70
src/session/view/content/room_details/general_page.rs

@ -15,7 +15,6 @@ use ruma::{
StateEventType,
room::{
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::RoomHistoryVisibilityEventContent,
power_levels::PowerLevelAction,
},
},
@ -26,8 +25,7 @@ use super::{MemberRow, RoomDetails, UpgradeDialog, UpgradeInfo};
use crate::{
Window,
components::{
Avatar, ButtonCountRow, CheckLoadingRow, ComboLoadingRow, CopyableRow, LoadingButton,
SwitchLoadingRow,
Avatar, ButtonCountRow, CheckLoadingRow, CopyableRow, LoadingButton, SwitchLoadingRow,
},
gettext_f,
prelude::*,
@ -96,7 +94,7 @@ mod imp {
#[template_child]
publish: TemplateChild<SwitchLoadingRow>,
#[template_child]
history_visibility: TemplateChild<ComboLoadingRow>,
history_visibility: TemplateChild<ButtonCountRow>,
#[template_child]
encryption: TemplateChild<SwitchLoadingRow>,
#[template_child]
@ -917,18 +915,15 @@ mod imp {
self.update_publish().await;
}
/// Update the history visibility edit button.
/// Update the history visibility row.
fn update_history_visibility(&self) {
let Some(room) = self.room.obj() else {
return;
};
let row = &self.history_visibility;
row.set_is_loading(false);
let visibility = room.history_visibility();
let history_visibility = room.history_visibility();
let text = match visibility {
let text = match history_visibility {
HistoryVisibilityValue::WorldReadable => {
gettext("Anyone, even if they are not in the room")
}
@ -941,55 +936,16 @@ mod imp {
}
HistoryVisibilityValue::Unsupported => gettext("Unsupported rule"),
};
row.set_selected_string(Some(text));
let is_supported = visibility != HistoryVisibilityValue::Unsupported;
let can_change = room
.permissions()
.is_allowed_to(PowerLevelAction::SendState(
StateEventType::RoomHistoryVisibility,
));
row.set_read_only(!is_supported || !can_change);
}
/// Set the history visibility of the room.
#[template_callback]
async fn set_history_visibility(&self) {
let Some(room) = self.room.obj() else {
return;
};
let row = &self.history_visibility;
let visibility = match row.selected() {
0 => HistoryVisibilityValue::WorldReadable,
1 => HistoryVisibilityValue::Shared,
2 => HistoryVisibilityValue::Joined,
3 => HistoryVisibilityValue::Invited,
_ => {
return;
}
};
self.history_visibility.set_subtitle(&text);
if room.history_visibility() == visibility {
// Nothing to do.
return;
}
row.set_is_loading(true);
row.set_read_only(true);
let content = RoomHistoryVisibilityEventContent::new(visibility.into());
let matrix_room = room.matrix_room().clone();
let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await });
if let Err(error) = handle.await.unwrap() {
error!("Could not change room history visibility: {error}");
toast!(self.obj(), gettext("Could not change who can read history"));
let can_change = history_visibility != HistoryVisibilityValue::Unsupported
&& room
.permissions()
.is_allowed_to(PowerLevelAction::SendState(
StateEventType::RoomHistoryVisibility,
));
self.update_history_visibility();
}
self.history_visibility.set_activatable(can_change);
}
/// Update the encryption row.

79
src/session/view/content/room_details/history_visibility_subpage.blp

@ -0,0 +1,79 @@
using Gtk 4.0;
using Adw 1;
template $RoomDetailsHistoryVisibilitySubpage: Adw.NavigationPage {
title: _("Who Can Read History");
styles [
"form-page",
]
child: Adw.ToolbarView {
[top]
Adw.HeaderBar {
show-back-button: false;
[start]
Gtk.Button {
icon-name: "go-previous-symbolic";
tooltip-text: _("Back");
clicked => $go_back() swapped;
styles [
"back",
]
}
[end]
$LoadingButton save_button {
sensitive: bind template.changed;
content-label: _("_Save");
use-underline: true;
clicked => $save() swapped;
styles [
"suggested-action",
]
}
}
content: Gtk.ScrolledWindow scrolled_window {
hscrollbar-policy: never;
propagate-natural-height: true;
child: Adw.Clamp {
child: Gtk.ListBox {
valign: start;
styles [
"boxed-list",
]
$CheckLoadingRow {
title: _("Anyone, even if they are not in the room");
action-name: "history-visibility.set-value";
action-target: "'world-readable'";
}
$CheckLoadingRow {
title: _("Members only, since this option was selected");
action-name: "history-visibility.set-value";
action-target: "'shared'";
}
$CheckLoadingRow {
title: _("Members only, since they were invited");
action-name: "history-visibility.set-value";
action-target: "'invited'";
}
$CheckLoadingRow {
title: _("Members only, since they joined the room");
action-name: "history-visibility.set-value";
action-target: "'joined'";
}
};
};
};
};
}

241
src/session/view/content/room_details/history_visibility_subpage.rs

@ -0,0 +1,241 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{glib, glib::clone};
use ruma::events::{
StateEventType,
room::{history_visibility::RoomHistoryVisibilityEventContent, power_levels::PowerLevelAction},
};
use tracing::error;
use crate::{
components::{CheckLoadingRow, LoadingButton, UnsavedChangesResponse, unsaved_changes_dialog},
session::model::{HistoryVisibilityValue, Room},
spawn_tokio, toast,
utils::BoundObjectWeakRef,
};
mod imp {
use std::cell::{Cell, RefCell};
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
#[template(
resource = "/org/gnome/Fractal/ui/session/view/content/room_details/history_visibility_subpage.ui"
)]
#[properties(wrapper_type = super::HistoryVisibilitySubpage)]
pub struct HistoryVisibilitySubpage {
#[template_child]
save_button: TemplateChild<LoadingButton>,
/// The presented room.
#[property(get, set = Self::set_room, explicit_notify, nullable)]
room: BoundObjectWeakRef<Room>,
/// The local value of the history visibility.
#[property(get, set = Self::set_local_value, explicit_notify, builder(HistoryVisibilityValue::default()))]
local_value: Cell<HistoryVisibilityValue>,
/// Whether the history visibility was changed by the user.
#[property(get)]
changed: Cell<bool>,
permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for HistoryVisibilitySubpage {
const NAME: &'static str = "RoomDetailsHistoryVisibilitySubpage";
type Type = super::HistoryVisibilitySubpage;
type ParentType = adw::NavigationPage;
fn class_init(klass: &mut Self::Class) {
CheckLoadingRow::ensure_type();
Self::bind_template(klass);
Self::bind_template_callbacks(klass);
klass.install_property_action("history-visibility.set-value", "local-value");
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for HistoryVisibilitySubpage {
fn dispose(&self) {
self.disconnect_signals();
}
}
impl WidgetImpl for HistoryVisibilitySubpage {}
impl NavigationPageImpl for HistoryVisibilitySubpage {}
#[gtk::template_callbacks]
impl HistoryVisibilitySubpage {
/// Set the presented room.
fn set_room(&self, room: Option<&Room>) {
let Some(room) = room else {
// Just ignore when room is missing.
return;
};
self.disconnect_signals();
let permissions_handler = room.permissions().connect_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update();
}
));
self.permissions_handler.replace(Some(permissions_handler));
let history_visibility_handler = room.connect_history_visibility_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update();
}
));
self.room.set(room, vec![history_visibility_handler]);
self.update();
self.obj().notify_room();
}
/// Update the subpage.
fn update(&self) {
let Some(room) = self.room.obj() else {
return;
};
self.set_local_value(room.history_visibility());
self.save_button.set_is_loading(false);
self.update_changed();
}
/// Set the local value of the history visibility.
fn set_local_value(&self, value: HistoryVisibilityValue) {
if self.local_value.get() == value {
return;
}
self.local_value.set(value);
self.update_changed();
self.obj().notify_local_value();
}
/// Whether we can change the history visibility.
fn can_change(&self) -> bool {
let Some(room) = self.room.obj() else {
return false;
};
if room.history_visibility() == HistoryVisibilityValue::Unsupported {
return false;
}
room.permissions()
.is_allowed_to(PowerLevelAction::SendState(
StateEventType::RoomHistoryVisibility,
))
}
/// Update whether the join rule was changed by the user.
#[template_callback]
fn update_changed(&self) {
let Some(room) = self.room.obj() else {
return;
};
let changed = if self.can_change() {
let current_join_rule = room.history_visibility();
let new_join_rule = self.local_value.get();
current_join_rule != new_join_rule
} else {
false
};
self.changed.set(changed);
self.obj().notify_changed();
}
/// Save the changes of this page.
#[template_callback]
async fn save(&self) {
if !self.changed.get() {
// Nothing to do.
return;
}
let Some(room) = self.room.obj() else {
return;
};
self.save_button.set_is_loading(true);
let content = RoomHistoryVisibilityEventContent::new(self.local_value.get().into());
let matrix_room = room.matrix_room().clone();
let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await });
if let Err(error) = handle.await.unwrap() {
error!("Could not change room history visibility: {error}");
toast!(self.obj(), gettext("Could not change who can read history"));
self.save_button.set_is_loading(false);
}
}
/// Go back to the previous page in the room details.
///
/// If there are changes in the page, ask the user to confirm.
#[template_callback]
async fn go_back(&self) {
let obj = self.obj();
let mut reset_after = false;
if self.changed.get() {
match unsaved_changes_dialog(&*obj).await {
UnsavedChangesResponse::Save => self.save().await,
UnsavedChangesResponse::Discard => reset_after = true,
UnsavedChangesResponse::Cancel => return,
}
}
let _ = obj.activate_action("navigation.pop", None);
if reset_after {
self.update();
}
}
/// Disconnect all the signal handlers.
fn disconnect_signals(&self) {
if let Some(room) = self.room.obj() {
if let Some(handler) = self.permissions_handler.take() {
room.permissions().disconnect(handler);
}
}
self.room.disconnect_signals();
}
}
}
glib::wrapper! {
/// Subpage to select the history visibility of a room.
pub struct HistoryVisibilitySubpage(ObjectSubclass<imp::HistoryVisibilitySubpage>)
@extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible;
}
impl HistoryVisibilitySubpage {
/// Construct a new `HistoryVisibilitySubpage` for the given room.
pub fn new(room: &Room) -> Self {
glib::Object::builder().property("room", room).build()
}
}

5
src/session/view/content/room_details/mod.rs

@ -11,6 +11,7 @@ mod addresses_subpage;
mod edit_details_subpage;
mod general_page;
mod history_viewer;
mod history_visibility_subpage;
mod invite_subpage;
mod join_rule_subpage;
mod member_row;
@ -26,6 +27,7 @@ use self::{
history_viewer::{
AudioHistoryViewer, FileHistoryViewer, HistoryViewerTimeline, VisualMediaHistoryViewer,
},
history_visibility_subpage::HistoryVisibilitySubpage,
invite_subpage::InviteSubpage,
join_rule_subpage::JoinRuleSubpage,
member_row::MemberRow,
@ -61,6 +63,8 @@ pub(super) enum SubpageName {
Permissions,
/// The page to edit the join rule of the room.
JoinRule,
/// The page to edit the history visibility of the room.
HistoryVisibility,
}
/// The view to present when opening the room details.
@ -241,6 +245,7 @@ mod imp {
PermissionsSubpage::new(&room.permissions()).upcast()
}
SubpageName::JoinRule => JoinRuleSubpage::new(room).upcast(),
SubpageName::HistoryVisibility => HistoryVisibilitySubpage::new(room).upcast(),
})
.clone()
}

2
src/ui-blueprint-resources.in

@ -32,7 +32,6 @@ components/power_level_selection/popover.blp
components/power_level_selection/row.blp
components/rows/button_count_row.blp
components/rows/check_loading_row.blp
components/rows/combo_loading_row.blp
components/rows/copyable_row.blp
components/rows/entry_add_row.blp
components/rows/loading_button_row.blp
@ -93,6 +92,7 @@ session/view/content/room_details/history_viewer/file.blp
session/view/content/room_details/history_viewer/file_row.blp
session/view/content/room_details/history_viewer/visual_media.blp
session/view/content/room_details/history_viewer/visual_media_item.blp
session/view/content/room_details/history_visibility_subpage.blp
session/view/content/room_details/invite_subpage/mod.blp
session/view/content/room_details/invite_subpage/row.blp
session/view/content/room_details/join_rule_subpage.blp

Loading…
Cancel
Save