message-row: Show the sending status of messages

Also logs if a sending error is encountered
This commit is contained in:
Kévin Commaille 2023-11-14 15:55:58 +01:00
parent b1de0cee42
commit 71611bc34e
No known key found for this signature in database
GPG key ID: 29A48C1F03620416
10 changed files with 367 additions and 55 deletions

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16px" viewBox="0 0 16 16" width="16px"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix color-interpolation-filters="sRGB" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="b"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.5"/></g></mask><clipPath id="c"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="d"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.7"/></g></mask><clipPath id="e"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="f"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.35"/></g></mask><clipPath id="g"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><g mask="url(#b)"><g clip-path="url(#c)" transform="matrix(1 0 0 1 -160 -80)"><path d="m 550 182 c -0.351562 0.003906 -0.695312 0.101562 -1 0.28125 v 3.4375 c 0.304688 0.179688 0.648438 0.277344 1 0.28125 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 c -0.339844 0 -0.679688 0.058594 -1 0.175781 v 6.824219 h 4 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#d)"><g clip-path="url(#e)" transform="matrix(1 0 0 1 -160 -80)"><path d="m 569 182 v 4 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 v 7 h 3 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#f)"><g clip-path="url(#g)" transform="matrix(1 0 0 1 -160 -80)"><path d="m 573 182.269531 v 3.449219 c 0.613281 -0.355469 0.996094 -1.007812 1 -1.71875 c 0 -0.714844 -0.382812 -1.375 -1 -1.730469 z m 0 4.90625 v 6.824219 h 2 v -4 c 0 -1.269531 -0.800781 -2.402344 -2 -2.824219 z m 0 0"/></g></g><path d="m 8 0 c -4.417969 0 -8 3.582031 -8 8 s 3.582031 8 8 8 s 8 -3.582031 8 -8 s -3.582031 -8 -8 -8 z m 3.164062 5.859375 c 0.640626 0.046875 0.933594 0.824219 0.476563 1.28125 l -3.640625 3.640625 c -0.292969 0.292969 -0.769531 0.292969 -1.0625 0 l -2.175781 -2.109375 c -0.707031 -0.710937 0.355469 -1.773437 1.0625 -1.0625 l 1.644531 1.578125 l 3.109375 -3.109375 c 0.15625 -0.152344 0.367187 -0.234375 0.585937 -0.21875 z m 0 0" fill="#2e3436"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -43,6 +43,7 @@
<file preprocess="xml-stripblanks">icons/scalable/status/checkmark-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/devices-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/document-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/done-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/empty-page-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/error-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/explore-symbolic.svg</file>

View file

@ -84,7 +84,8 @@ src/session/view/content/room_history/message_row/content.rs
src/session/view/content/room_history/message_row/file.ui
src/session/view/content/room_history/message_row/location.rs
src/session/view/content/room_history/message_row/media.rs
src/session/view/content/room_history/message_row/mod.ui
src/session/view/content/room_history/message_row/message_state_stack.rs
src/session/view/content/room_history/message_row/message_state_stack.ui
src/session/view/content/room_history/message_toolbar/attachment_dialog.ui
src/session/view/content/room_history/message_toolbar/mod.rs
src/session/view/content/room_history/message_toolbar/mod.ui

View file

@ -12,10 +12,10 @@ pub use self::{
avatar::{AvatarData, AvatarImage, AvatarUriSource},
notifications::Notifications,
room::{
Event, EventKey, HighlightFlags, Member, MemberList, MemberRole, Membership, PowerLevel,
ReactionGroup, ReactionList, Room, RoomType, Timeline, TimelineItem, TimelineItemExt,
TimelineState, TypingList, UserReadReceipt, VirtualItem, VirtualItemKind, POWER_LEVEL_MAX,
POWER_LEVEL_MIN,
Event, EventKey, HighlightFlags, Member, MemberList, MemberRole, Membership, MessageState,
PowerLevel, ReactionGroup, ReactionList, Room, RoomType, Timeline, TimelineItem,
TimelineItemExt, TimelineState, TypingList, UserReadReceipt, VirtualItem, VirtualItemKind,
POWER_LEVEL_MAX, POWER_LEVEL_MIN,
},
room_list::RoomList,
session::{Session, SessionState},

View file

@ -3,14 +3,15 @@ use std::{borrow::Cow, fmt};
use gtk::{gio, glib, prelude::*, subclass::prelude::*};
use indexmap::IndexMap;
use matrix_sdk_ui::timeline::{
AnyOtherFullStateEventContent, Error as TimelineError, EventTimelineItem, RepliedToEvent,
TimelineDetails, TimelineItemContent,
AnyOtherFullStateEventContent, Error as TimelineError, EventSendState, EventTimelineItem,
RepliedToEvent, TimelineDetails, TimelineItemContent,
};
use ruma::{
events::{receipt::Receipt, room::message::MessageType, AnySyncTimelineEvent},
serde::Raw,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId,
};
use tracing::error;
mod reaction_group;
mod reaction_list;
@ -67,6 +68,18 @@ impl glib::FromVariant for EventKey {
}
}
#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "MessageState")]
pub enum MessageState {
#[default]
None = 0,
Sending = 1,
Error = 2,
Cancelled = 3,
Edited = 4,
}
#[derive(Clone, Debug, glib::Boxed)]
#[boxed_type(name = "BoxedEventTimelineItem")]
pub struct BoxedEventTimelineItem(EventTimelineItem);
@ -79,7 +92,7 @@ pub struct UserReadReceipt {
}
mod imp {
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use glib::object::WeakRef;
use once_cell::sync::Lazy;
@ -99,6 +112,9 @@ mod imp {
/// The read receipts on this event.
pub read_receipts: gio::ListStore,
/// The state of this event.
pub state: Cell<MessageState>,
}
impl Default for Event {
@ -108,6 +124,7 @@ mod imp {
room: Default::default(),
reactions: Default::default(),
read_receipts: gio::ListStore::new::<glib::BoxedAnyObject>(),
state: Default::default(),
}
}
}
@ -146,6 +163,9 @@ mod imp {
glib::ParamSpecBoolean::builder("has-read-receipts")
.read_only()
.build(),
glib::ParamSpecEnum::builder::<MessageState>("state")
.read_only()
.build(),
]
});
@ -179,6 +199,7 @@ mod imp {
"is-highlighted" => obj.is_highlighted().to_value(),
"read-receipts" => obj.read_receipts().to_value(),
"has-read-receipts" => obj.has_read_receipts().to_value(),
"state" => obj.state().to_value(),
_ => unimplemented!(),
}
}
@ -291,6 +312,7 @@ impl Event {
if self.is_highlighted() != was_highlighted {
self.notify("is-highlighted");
}
self.update_state();
}
/// The raw JSON source for this `Event`, if it has been echoed back
@ -447,6 +469,51 @@ impl Event {
}
}
/// The state of this `Event`.
pub fn state(&self) -> MessageState {
self.imp().state.get()
}
/// Compute the current state of this `Event`.
fn compute_state(&self) -> MessageState {
let item_ref = self.imp().item.borrow();
let Some(item) = item_ref.as_ref() else {
return MessageState::None;
};
if let Some(send_state) = item.send_state() {
match send_state {
EventSendState::NotSentYet => return MessageState::Sending,
EventSendState::SendingFailed { error } => {
if self.state() != MessageState::Error {
error!("Failed to send message: {error}");
}
return MessageState::Error;
}
EventSendState::Cancelled => return MessageState::Cancelled,
EventSendState::Sent { .. } => {}
}
}
match item.content() {
TimelineItemContent::Message(msg) if msg.is_edited() => MessageState::Edited,
_ => MessageState::None,
}
}
/// Update the state of this `Event`.
fn update_state(&self) {
let state = self.compute_state();
if self.state() == state {
return;
}
self.imp().state.set(state);
self.notify("state");
}
/// Whether this `Event` should be highlighted.
pub fn is_highlighted(&self) -> bool {
let item_ref = self.imp().item.borrow();

View file

@ -0,0 +1,164 @@
use adw::subclass::prelude::*;
use gettextrs::gettext;
use gtk::{glib, glib::clone, prelude::*, CompositeTemplate};
use crate::session::model::MessageState;
mod imp {
use std::cell::Cell;
use glib::subclass::InitializingObject;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/message_state_stack.ui"
)]
pub struct MessageStateStack {
/// The state that is currently displayed.
pub state: Cell<MessageState>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub error_image: TemplateChild<gtk::Image>,
}
#[glib::object_subclass]
impl ObjectSubclass for MessageStateStack {
const NAME: &'static str = "MessageStateStack";
type Type = super::MessageStateStack;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for MessageStateStack {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecEnum::builder::<MessageState>("state")
.explicit_notify()
.build()]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"state" => {
obj.set_state(value.get().unwrap());
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"state" => obj.state().to_value(),
_ => unimplemented!(),
}
}
}
impl WidgetImpl for MessageStateStack {}
impl BinImpl for MessageStateStack {}
}
glib::wrapper! {
/// A stack to display the different message states.
pub struct MessageStateStack(ObjectSubclass<imp::MessageStateStack>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl MessageStateStack {
/// Create a new `MessageStateStack`.
pub fn new() -> Self {
glib::Object::new()
}
/// The state that is currently displayed.
pub fn state(&self) -> MessageState {
self.imp().state.get()
}
/// Set the state to display.
pub fn set_state(&self, state: MessageState) {
let prev_state = self.state();
if prev_state == state {
return;
}
let imp = self.imp();
let stack = &*imp.stack;
match state {
MessageState::None => {
if matches!(
prev_state,
MessageState::Sending | MessageState::Error | MessageState::Cancelled
) {
// Show the sent icon for 2 seconds.
stack.set_visible_child_name("sent");
glib::timeout_add_seconds_local_once(
2,
clone!(@weak self as obj => move || {
obj.set_visible(false);
}),
);
} else {
self.set_visible(false);
}
}
MessageState::Sending => {
stack.set_visible_child_name("sending");
self.set_visible(true);
}
MessageState::Error => {
imp.error_image
.set_tooltip_text(Some(&gettext("Could not send the message")));
stack.set_visible_child_name("error");
self.set_visible(true);
}
MessageState::Cancelled => {
imp.error_image
.set_tooltip_text(Some(&gettext("An error occurred with the sending queue")));
stack.set_visible_child_name("error");
self.set_visible(true);
}
MessageState::Edited => {
if matches!(
prev_state,
MessageState::Sending | MessageState::Error | MessageState::Cancelled
) {
// Show the sent icon for 2 seconds.
stack.set_visible_child_name("sent");
glib::timeout_add_seconds_local_once(
2,
clone!(@weak stack => move || {
stack.set_visible_child_name("edited");
}),
);
} else {
stack.set_visible_child_name("edited");
self.set_visible(true);
}
}
}
imp.state.set(state);
self.notify("state");
}
}

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="MessageStateStack" parent="AdwBin">
<child>
<object class="GtkStack" id="stack">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkStackPage">
<property name="name">sending</property>
<property name="child">
<object class="Spinner" id="spinner">
<property name="valign">center</property>
<!-- Translators: As in 'Sending message…'. -->
<property name="tooltip-text" translatable="yes">Sending…</property>
<accessibility>
<!-- Translators: As in 'Sending message…'. -->
<property name="label" translatable="yes">Sending…</property>
</accessibility>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">error</property>
<property name="child">
<object class="GtkImage" id="error_image">
<property name="valign">center</property>
<property name="icon-name">error-symbolic</property>
<style>
<class name="error"/>
</style>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">sent</property>
<property name="child">
<object class="GtkImage">
<property name="valign">center</property>
<property name="icon-name">done-symbolic</property>
<!-- Translators: As in 'Sent message'. -->
<property name="tooltip-text" translatable="yes">Sent</property>
<accessibility>
<!-- Translators: As in 'Sent message'. -->
<property name="label" translatable="yes">Sent</property>
</accessibility>
<style>
<class name="success"/>
</style>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">edited</property>
<property name="child">
<object class="GtkImage">
<style>
<class name="dim-label"/>
</style>
<property name="icon-name">edit-symbolic</property>
<!-- Translators: As in 'Edited message'. -->
<property name="tooltip-text" translatable="yes">Edited</property>
<accessibility>
<!-- Translators: As in 'Edited message'. -->
<property name="label" translatable="yes">Edited</property>
</accessibility>
</object>
</property>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -3,24 +3,23 @@ mod content;
mod file;
mod location;
mod media;
mod message_state_stack;
mod reaction;
mod reaction_list;
mod reply;
mod text;
use adw::{prelude::*, subclass::prelude::*};
use gtk::{
gdk, glib,
glib::{clone, signal::SignalHandlerId},
CompositeTemplate,
};
use gtk::{gdk, glib, glib::clone, CompositeTemplate};
use matrix_sdk::ruma::events::room::message::MessageType;
use tracing::warn;
pub use self::content::{ContentFormat, MessageContent};
use self::{media::MessageMedia, reaction_list::MessageReactionList};
use self::{
media::MessageMedia, message_state_stack::MessageStateStack, reaction_list::MessageReactionList,
};
use super::ReadReceiptsList;
use crate::{components::Avatar, prelude::*, session::model::Event, Window};
use crate::{components::Avatar, prelude::*, session::model::Event, utils::BoundObject, Window};
mod imp {
use std::cell::RefCell;
@ -46,12 +45,13 @@ mod imp {
#[template_child]
pub content: TemplateChild<MessageContent>,
#[template_child]
pub message_state: TemplateChild<MessageStateStack>,
#[template_child]
pub reactions: TemplateChild<MessageReactionList>,
#[template_child]
pub read_receipts: TemplateChild<ReadReceiptsList>,
pub source_changed_handler: RefCell<Option<SignalHandlerId>>,
pub bindings: RefCell<Vec<glib::Binding>>,
pub event: RefCell<Option<Event>>,
pub event: BoundObject<Event>,
}
#[glib::object_subclass]
@ -118,6 +118,14 @@ mod imp {
),
);
}
fn dispose(&self) {
self.event.disconnect_signals();
while let Some(binding) = self.bindings.borrow_mut().pop() {
binding.unbind();
}
}
}
impl WidgetImpl for MessageRow {}
@ -164,20 +172,16 @@ impl MessageRow {
}
pub fn event(&self) -> Option<Event> {
self.imp().event.borrow().clone()
self.imp().event.obj()
}
pub fn set_event(&self, event: Event) {
let imp = self.imp();
// Remove signals and bindings from the previous event
if let Some(event) = imp.event.take() {
if let Some(source_changed_handler) = imp.source_changed_handler.take() {
event.disconnect(source_changed_handler);
}
while let Some(binding) = imp.bindings.borrow_mut().pop() {
binding.unbind();
}
// Remove signals and bindings from the previous event.
imp.event.disconnect_signals();
while let Some(binding) = imp.bindings.borrow_mut().pop() {
binding.unbind();
}
imp.avatar
@ -199,25 +203,30 @@ impl MessageRow {
.sync_create()
.build();
let state_binding = event
.bind_property("state", &*imp.message_state, "state")
.sync_create()
.build();
imp.bindings.borrow_mut().append(&mut vec![
display_name_binding,
show_header_binding,
timestamp_binding,
state_binding,
]);
imp.source_changed_handler
.replace(Some(event.connect_notify_local(
Some("source"),
clone!(@weak self as obj => move |event, _| {
obj.update_content(event);
}),
)));
let source_handler = event.connect_notify_local(
Some("source"),
clone!(@weak self as obj => move |event, _| {
obj.update_content(event);
}),
);
self.update_content(&event);
imp.reactions
.set_reaction_list(&event.room().get_or_create_members(), event.reactions());
imp.read_receipts.set_source(event.read_receipts());
imp.event.replace(Some(event));
imp.event.set(event, vec![source_handler]);
self.notify("event");
}
@ -232,12 +241,10 @@ impl MessageRow {
/// Open the media viewer with the media content of this row.
fn show_media(&self) {
let imp = self.imp();
let Some(window) = self.root().and_downcast::<Window>() else {
return;
};
let borrowed_event = imp.event.borrow();
let Some(event) = borrowed_event.as_ref() else {
let Some(event) = self.event() else {
return;
};
let Some(message) = event.message() else {
@ -245,13 +252,17 @@ impl MessageRow {
};
if matches!(message, MessageType::Image(_) | MessageType::Video(_)) {
let Some(media_widget) = imp.content.content_widget().and_downcast::<MessageMedia>()
let Some(media_widget) = self
.imp()
.content
.content_widget()
.and_downcast::<MessageMedia>()
else {
warn!("Trying to show media of a non-media message");
return;
};
window.session_view().show_media(event, &media_widget);
window.session_view().show_media(&event, &media_widget);
}
}
}

View file

@ -62,22 +62,8 @@
</object>
</child>
<child>
<object class="GtkImage">
<style>
<class name="dim-label"/>
</style>
<property name="icon-name">edit-symbolic</property>
<!-- Translators: As in 'Edited message'. -->
<property name="tooltip-text" translatable="yes">Edited</property>
<accessibility>
<!-- Translators: As in 'Edited message'. -->
<property name="label" translatable="yes">Edited</property>
</accessibility>
<binding name="visible">
<lookup name="is-edited" type="RoomEvent">
<lookup name="event">ContentMessageRow</lookup>
</lookup>
</binding>
<object class="MessageStateStack" id="message_state">
<property name="visible">false</property>
<layout>
<property name="column">2</property>
<property name="row">1</property>

View file

@ -67,6 +67,7 @@
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/file.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/location.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/media.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/message_state_stack.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/reaction/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/reaction/reaction_popover.ui</file>