mirror of
https://gitlab.gnome.org/World/fractal.git
synced 2025-06-03 00:01:51 -04:00
room-history: Remove banner for tombstoned rooms
Instead we offer to join or view the new room instead of showing the composer. This also makes sure that the state of the Join/View button is updated depending on the joining state of the successor.
This commit is contained in:
parent
9f426e2c6d
commit
893f795228
@ -1469,8 +1469,8 @@ impl Room {
|
||||
}
|
||||
|
||||
/// The ID of the successor of this Room, if this room was upgraded.
|
||||
pub(crate) fn successor_id(&self) -> Option<&RoomId> {
|
||||
self.imp().successor_id.get().map(std::ops::Deref::deref)
|
||||
pub(crate) fn successor_id(&self) -> Option<&OwnedRoomId> {
|
||||
self.imp().successor_id.get()
|
||||
}
|
||||
|
||||
/// The `matrix.to` URI representation for this room.
|
||||
|
@ -37,10 +37,10 @@ use self::{
|
||||
};
|
||||
use super::message_row::MessageContent;
|
||||
use crate::{
|
||||
components::{CustomEntry, LabelWithWidgets},
|
||||
components::{CustomEntry, LabelWithWidgets, LoadingButton},
|
||||
gettext_f,
|
||||
prelude::*,
|
||||
session::model::{Event, Member, Room, Timeline},
|
||||
session::model::{Event, Member, Room, RoomListRoomInfo, Timeline},
|
||||
spawn, spawn_tokio, toast,
|
||||
utils::{
|
||||
media::{
|
||||
@ -49,11 +49,24 @@ use crate::{
|
||||
},
|
||||
Location, LocationError, TemplateCallbacks, TokioDrop,
|
||||
},
|
||||
Application, Window,
|
||||
};
|
||||
|
||||
/// A map of composer state per-session and per-room.
|
||||
type ComposerStatesMap = HashMap<Option<String>, HashMap<Option<OwnedRoomId>, ComposerState>>;
|
||||
|
||||
/// The available stack pages of the [`MessageToolbar`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::AsRefStr, strum::EnumString)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
enum MessageToolbarPage {
|
||||
/// The composer and other buttons to send messages.
|
||||
Composer,
|
||||
/// The user is not allowed to send messages in the room.
|
||||
NoPermission,
|
||||
/// The room was tombstoned.
|
||||
Tombstoned,
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
@ -63,7 +76,6 @@ mod imp {
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
use super::*;
|
||||
use crate::Application;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
|
||||
#[template(
|
||||
@ -81,9 +93,15 @@ mod imp {
|
||||
related_event_header: TemplateChild<LabelWithWidgets>,
|
||||
#[template_child]
|
||||
related_event_content: TemplateChild<MessageContent>,
|
||||
#[template_child]
|
||||
tombstoned_label: TemplateChild<gtk::Label>,
|
||||
#[template_child]
|
||||
tombstoned_button: TemplateChild<LoadingButton>,
|
||||
/// The timeline used to send messages.
|
||||
#[property(get, set = Self::set_timeline, explicit_notify, nullable)]
|
||||
timeline: glib::WeakRef<Timeline>,
|
||||
successor_room_list_info: RoomListRoomInfo,
|
||||
room_handlers: RefCell<Vec<glib::SignalHandlerId>>,
|
||||
send_message_permission_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
/// Whether outgoing messages should be interpreted as markdown.
|
||||
#[property(get, set)]
|
||||
@ -150,30 +168,53 @@ mod imp {
|
||||
// Location.
|
||||
let location = Location::new();
|
||||
obj.action_set_enabled("message-toolbar.send-location", location.is_available());
|
||||
|
||||
// Listen to changes in the room list for the successor.
|
||||
self.successor_room_list_info
|
||||
.connect_is_joining_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_tombstoned_page();
|
||||
}
|
||||
));
|
||||
self.successor_room_list_info
|
||||
.connect_local_room_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_tombstoned_page();
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.completion.unparent();
|
||||
|
||||
if let Some(timeline) = self.timeline.upgrade() {
|
||||
if let Some(handler) = self.send_message_permission_handler.take() {
|
||||
timeline.room().permissions().disconnect(handler);
|
||||
}
|
||||
}
|
||||
self.disconnect_signals();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MessageToolbar {
|
||||
fn grab_focus(&self) -> bool {
|
||||
if self
|
||||
let Some(visible_page) = self
|
||||
.main_stack
|
||||
.visible_child_name()
|
||||
.is_none_or(|name| name == "disabled")
|
||||
{
|
||||
.and_then(|name| MessageToolbarPage::try_from(name.as_str()).ok())
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
self.message_entry.grab_focus()
|
||||
match visible_page {
|
||||
MessageToolbarPage::Composer => self.message_entry.grab_focus(),
|
||||
MessageToolbarPage::NoPermission => false,
|
||||
MessageToolbarPage::Tombstoned => {
|
||||
if self.tombstoned_button.is_visible() {
|
||||
self.tombstoned_button.grab_focus()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,13 +230,39 @@ mod imp {
|
||||
}
|
||||
let obj = self.obj();
|
||||
|
||||
if let Some(timeline) = &old_timeline {
|
||||
if let Some(handler) = self.send_message_permission_handler.take() {
|
||||
timeline.room().permissions().disconnect(handler);
|
||||
}
|
||||
}
|
||||
self.disconnect_signals();
|
||||
|
||||
if let Some(timeline) = timeline {
|
||||
let room = timeline.room();
|
||||
|
||||
let is_tombstoned_handler = room.connect_is_tombstoned_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_visible_page();
|
||||
}
|
||||
));
|
||||
let successor_id_handler = room.connect_successor_id_string_notify(clone!(
|
||||
#[weak(rename_to= imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_successor_identifier();
|
||||
imp.update_tombstoned_page();
|
||||
}
|
||||
));
|
||||
let successor_handler = room.connect_successor_notify(clone!(
|
||||
#[weak(rename_to= imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_tombstoned_page();
|
||||
}
|
||||
));
|
||||
self.room_handlers.replace(vec![
|
||||
is_tombstoned_handler,
|
||||
successor_id_handler,
|
||||
successor_handler,
|
||||
]);
|
||||
|
||||
let send_message_permission_handler = timeline
|
||||
.room()
|
||||
.permissions()
|
||||
@ -203,41 +270,105 @@ mod imp {
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.send_message_permission_updated();
|
||||
imp.update_visible_page();
|
||||
}
|
||||
));
|
||||
self.send_message_permission_handler
|
||||
.replace(Some(send_message_permission_handler));
|
||||
|
||||
if let Some(session) = room.session() {
|
||||
self.successor_room_list_info
|
||||
.set_room_list(session.room_list());
|
||||
}
|
||||
}
|
||||
|
||||
self.completion.set_room(timeline.map(Timeline::room));
|
||||
self.timeline.set(timeline);
|
||||
|
||||
self.send_message_permission_updated();
|
||||
self.update_successor_identifier();
|
||||
self.update_tombstoned_page();
|
||||
self.update_visible_page();
|
||||
|
||||
obj.notify_timeline();
|
||||
self.update_current_composer_state(old_timeline);
|
||||
}
|
||||
|
||||
/// The stack page that should be presented given the current state.
|
||||
fn visible_page(&self) -> MessageToolbarPage {
|
||||
let Some(room) = self.timeline.upgrade().map(|timeline| timeline.room()) else {
|
||||
return MessageToolbarPage::NoPermission;
|
||||
};
|
||||
|
||||
if room.is_tombstoned() {
|
||||
MessageToolbarPage::Tombstoned
|
||||
} else if room.permissions().can_send_message() {
|
||||
MessageToolbarPage::Composer
|
||||
} else {
|
||||
MessageToolbarPage::NoPermission
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the user can compose a message.
|
||||
///
|
||||
/// It depends on whether our own user has the permission to send a
|
||||
/// message in the current room.
|
||||
pub(super) fn can_compose_message(&self) -> bool {
|
||||
self.timeline
|
||||
.upgrade()
|
||||
.is_some_and(|timeline| timeline.room().permissions().can_send_message())
|
||||
self.visible_page() == MessageToolbarPage::Composer
|
||||
}
|
||||
|
||||
/// Handle an update of the permission to send a message in the current
|
||||
/// Update the visible stack page.
|
||||
fn update_visible_page(&self) {
|
||||
self.main_stack
|
||||
.set_visible_child_name(self.visible_page().as_ref());
|
||||
}
|
||||
|
||||
/// Update the identifier to watch for the successor of the current
|
||||
/// room.
|
||||
fn send_message_permission_updated(&self) {
|
||||
let page = if self.can_compose_message() {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
fn update_successor_identifier(&self) {
|
||||
let successor_id = self
|
||||
.timeline
|
||||
.upgrade()
|
||||
.and_then(|timeline| timeline.room().successor_id().cloned());
|
||||
self.successor_room_list_info
|
||||
.set_identifiers(successor_id.into_iter().map(Into::into).collect());
|
||||
}
|
||||
|
||||
/// Update the tombstoned stack page.
|
||||
fn update_tombstoned_page(&self) {
|
||||
let Some(room) = self.timeline.upgrade().map(|timeline| timeline.room()) else {
|
||||
return;
|
||||
};
|
||||
self.main_stack.set_visible_child_name(page);
|
||||
|
||||
// A "real" successor must have the current room as a predecessor. We still want
|
||||
// to show the "View" button if it is only the room that matches the successor
|
||||
// ID.
|
||||
let has_successor_room =
|
||||
room.successor().is_some() || self.successor_room_list_info.local_room().is_some();
|
||||
let has_successor_id = room.successor_id().is_some();
|
||||
let has_successor = has_successor_room || has_successor_id;
|
||||
|
||||
// Update description.
|
||||
let description = if has_successor {
|
||||
gettext("The conversation continues in a new room")
|
||||
} else {
|
||||
gettext("The conversation has ended")
|
||||
};
|
||||
self.tombstoned_label.set_label(&description);
|
||||
|
||||
// Update button.
|
||||
if has_successor {
|
||||
let label = if has_successor_room {
|
||||
// Translators: This is a verb, as in 'View Room'.
|
||||
gettext("View")
|
||||
} else {
|
||||
gettext("Join")
|
||||
};
|
||||
self.tombstoned_button.set_content_label(label);
|
||||
|
||||
let is_joining_successor = self.successor_room_list_info.is_joining();
|
||||
self.tombstoned_button.set_is_loading(is_joining_successor);
|
||||
}
|
||||
self.tombstoned_button.set_visible(has_successor);
|
||||
}
|
||||
|
||||
/// Whether the buffer of the composer is empty.
|
||||
@ -1044,6 +1175,62 @@ mod imp {
|
||||
|
||||
room.send_typing_notification(typing);
|
||||
}
|
||||
|
||||
/// Join or view the successor of the room, if possible.
|
||||
#[template_callback]
|
||||
async fn join_or_view_successor(&self) {
|
||||
let Some(room) = self.timeline.upgrade().map(|timeline| timeline.room()) else {
|
||||
return;
|
||||
};
|
||||
let Some(session) = room.session() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !room.is_tombstoned() {
|
||||
return;
|
||||
}
|
||||
let obj = self.obj();
|
||||
|
||||
if let Some(successor) = room
|
||||
.successor()
|
||||
.or_else(|| self.successor_room_list_info.local_room())
|
||||
{
|
||||
let Some(window) = obj.root().and_downcast::<Window>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
window.session_view().select_room(successor);
|
||||
} else if let Some(successor_id) = room.successor_id().cloned() {
|
||||
let via = successor_id
|
||||
.server_name()
|
||||
.map(ToOwned::to_owned)
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
if let Err(error) = session
|
||||
.room_list()
|
||||
.join_by_id_or_alias(successor_id.into(), via)
|
||||
.await
|
||||
{
|
||||
toast!(obj, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Disconnect the signal handlers of this toolbar.
|
||||
fn disconnect_signals(&self) {
|
||||
if let Some(timeline) = self.timeline.upgrade() {
|
||||
let room = timeline.room();
|
||||
|
||||
for handler in self.room_handlers.take() {
|
||||
room.disconnect(handler);
|
||||
}
|
||||
|
||||
if let Some(handler) = self.send_message_permission_handler.take() {
|
||||
room.permissions().disconnect(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">enabled</property>
|
||||
<property name="name">composer</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
@ -169,7 +169,7 @@
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">disabled</property>
|
||||
<property name="name">no-permission</property>
|
||||
<property name="child">
|
||||
<object class="AdwClamp">
|
||||
<property name="hexpand">True</property>
|
||||
@ -193,6 +193,50 @@
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">tombstoned</property>
|
||||
<property name="child">
|
||||
<object class="AdwClamp">
|
||||
<property name="hexpand">True</property>
|
||||
<style>
|
||||
<class name="composer-replacement"/>
|
||||
</style>
|
||||
<property name="child">
|
||||
<object class="AdwWrapBox">
|
||||
<property name="child-spacing">12</property>
|
||||
<property name="line-spacing">12</property>
|
||||
<property name="justify">fill</property>
|
||||
<property name="justify-last-line">True</property>
|
||||
<property name="halign">center</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="tombstoned_label">
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="justify">center</property>
|
||||
<style>
|
||||
<class name="heading"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="LoadingButton" id="tombstoned_button">
|
||||
<property name="halign">center</property>
|
||||
<signal name="clicked" handler="join_or_view_successor" swapped="yes"/>
|
||||
<style>
|
||||
<class name="text-button"/>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
|
@ -47,7 +47,6 @@ use crate::{
|
||||
},
|
||||
spawn, toast,
|
||||
utils::{BoundObject, GroupingListGroup, GroupingListModel, LoadingState, TemplateCallbacks},
|
||||
Window,
|
||||
};
|
||||
|
||||
/// The time to wait before considering that scrolling has ended.
|
||||
@ -96,8 +95,6 @@ mod imp {
|
||||
#[template_child]
|
||||
stack: TemplateChild<gtk::Stack>,
|
||||
#[template_child]
|
||||
tombstoned_banner: TemplateChild<adw::Banner>,
|
||||
#[template_child]
|
||||
drag_overlay: TemplateChild<DragOverlay>,
|
||||
/// The context menu for rows presenting an [`Event`].
|
||||
event_context_menu: OnceCell<EventActionsContextMenu>,
|
||||
@ -123,7 +120,7 @@ mod imp {
|
||||
grouping_model: OnceCell<GroupingListModel>,
|
||||
scroll_timeout: RefCell<Option<glib::SourceId>>,
|
||||
read_timeout: RefCell<Option<glib::SourceId>>,
|
||||
room_handlers: RefCell<Vec<glib::SignalHandlerId>>,
|
||||
room_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
can_invite_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
membership_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
join_rule_handler: RefCell<Option<glib::SignalHandlerId>>,
|
||||
@ -391,7 +388,7 @@ mod imp {
|
||||
/// Disconnect all the signals.
|
||||
fn disconnect_all(&self) {
|
||||
if let Some(room) = self.room() {
|
||||
for handler in self.room_handlers.take() {
|
||||
if let Some(handler) = self.room_handler.take() {
|
||||
room.disconnect(handler);
|
||||
}
|
||||
|
||||
@ -466,34 +463,8 @@ mod imp {
|
||||
imp.update_invite_action();
|
||||
}
|
||||
));
|
||||
let tombstoned_handler = room.connect_is_tombstoned_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_tombstoned_banner();
|
||||
}
|
||||
));
|
||||
let successor_id_handler = room.connect_successor_id_string_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_tombstoned_banner();
|
||||
}
|
||||
));
|
||||
let successor_handler = room.connect_successor_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
self,
|
||||
move |_| {
|
||||
imp.update_tombstoned_banner();
|
||||
}
|
||||
));
|
||||
|
||||
self.room_handlers.replace(vec![
|
||||
is_direct_handler,
|
||||
tombstoned_handler,
|
||||
successor_id_handler,
|
||||
successor_handler,
|
||||
]);
|
||||
self.room_handler.replace(Some(is_direct_handler));
|
||||
|
||||
let empty_handler = timeline.connect_is_empty_notify(clone!(
|
||||
#[weak(rename_to = imp)]
|
||||
@ -533,7 +504,6 @@ mod imp {
|
||||
self.update_view();
|
||||
self.load_more_events_if_needed();
|
||||
self.update_room_menu();
|
||||
self.update_tombstoned_banner();
|
||||
self.update_invite_action();
|
||||
|
||||
self.obj().notify_timeline();
|
||||
@ -931,36 +901,6 @@ mod imp {
|
||||
None
|
||||
}
|
||||
|
||||
/// Update the tombstoned banner according to the state of the current
|
||||
/// room.
|
||||
fn update_tombstoned_banner(&self) {
|
||||
let banner = &self.tombstoned_banner;
|
||||
|
||||
let Some(room) = self.room() else {
|
||||
banner.set_revealed(false);
|
||||
return;
|
||||
};
|
||||
|
||||
if !room.is_tombstoned() {
|
||||
banner.set_revealed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if room.successor().is_some() {
|
||||
banner.set_title(&gettext("There is a newer version of this room"));
|
||||
// Translators: This is a verb, as in 'View Room'.
|
||||
banner.set_button_label(Some(&gettext("View")));
|
||||
} else if room.successor_id().is_some() {
|
||||
banner.set_title(&gettext("There is a newer version of this room"));
|
||||
banner.set_button_label(Some(&gettext("Join")));
|
||||
} else {
|
||||
banner.set_title(&gettext("This room was closed"));
|
||||
banner.set_button_label(None);
|
||||
}
|
||||
|
||||
banner.set_revealed(true);
|
||||
}
|
||||
|
||||
/// Leave the room.
|
||||
async fn leave(&self) {
|
||||
let Some(room) = self.room() else {
|
||||
@ -1042,44 +982,6 @@ mod imp {
|
||||
.action_set_enabled("room-history.invite-members", can_invite);
|
||||
}
|
||||
|
||||
/// Join or view the successor of the room, if possible.
|
||||
#[template_callback]
|
||||
async fn join_or_view_successor(&self) {
|
||||
let Some(room) = self.room() else {
|
||||
return;
|
||||
};
|
||||
let Some(session) = room.session() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !room.is_joined() || !room.is_tombstoned() {
|
||||
return;
|
||||
}
|
||||
let obj = self.obj();
|
||||
|
||||
if let Some(successor) = room.successor() {
|
||||
let Some(window) = obj.root().and_downcast::<Window>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
window.session_view().select_room(successor);
|
||||
} else if let Some(successor_id) = room.successor_id().map(ToOwned::to_owned) {
|
||||
let via = successor_id
|
||||
.server_name()
|
||||
.map(ToOwned::to_owned)
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
if let Err(error) = session
|
||||
.room_list()
|
||||
.join_by_id_or_alias(successor_id.into(), via)
|
||||
.await
|
||||
{
|
||||
toast!(obj, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The context menu for rows presenting an [`Event`].
|
||||
pub(super) fn event_context_menu(&self) -> &EventActionsContextMenu {
|
||||
self.event_context_menu.get_or_init(Default::default)
|
||||
|
@ -157,11 +157,6 @@
|
||||
<property name="content">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwBanner" id="tombstoned_banner">
|
||||
<signal name="button-clicked" handler="join_or_view_successor" swapped="yes"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="ContentVerificationInfoBar" id="verification_info_bar">
|
||||
<binding name="verification">
|
||||
|
Loading…
x
Reference in New Issue
Block a user