fractal/session/view/content/room_history/state_row/
mod.rsmod creation;
mod tombstone;
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{glib, CompositeTemplate};
use matrix_sdk_ui::timeline::{
AnyOtherFullStateEventContent, MemberProfileChange, MembershipChange, OtherState,
RoomMembershipChange, TimelineItemContent,
};
use ruma::{
events::{room::member::MembershipState, FullStateEventContent},
UserId,
};
use tracing::warn;
use self::{creation::StateCreation, tombstone::StateTombstone};
use super::ReadReceiptsList;
use crate::{gettext_f, prelude::*, session::model::Event};
mod imp {
use std::cell::RefCell;
use glib::subclass::InitializingObject;
use super::*;
use crate::utils::template_callbacks::TemplateCallbacks;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/state_row/mod.ui"
)]
#[properties(wrapper_type = super::StateRow)]
pub struct StateRow {
#[template_child]
pub content: TemplateChild<adw::Bin>,
#[template_child]
pub read_receipts: TemplateChild<ReadReceiptsList>,
#[property(get, set = Self::set_event)]
pub event: RefCell<Option<Event>>,
}
#[glib::object_subclass]
impl ObjectSubclass for StateRow {
const NAME: &'static str = "ContentStateRow";
type Type = super::StateRow;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
TemplateCallbacks::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for StateRow {}
impl WidgetImpl for StateRow {}
impl BinImpl for StateRow {}
impl StateRow {
fn set_event(&self, event: Event) {
let obj = self.obj();
match event.content() {
TimelineItemContent::MembershipChange(membership_change) => {
obj.update_with_membership_change(&membership_change, &event.sender_id())
}
TimelineItemContent::ProfileChange(profile_change) => obj
.update_with_profile_change(
&profile_change,
&event.sender().disambiguated_name(),
),
TimelineItemContent::OtherState(other_state) => {
obj.update_with_other_state(&event, &other_state)
}
_ => unreachable!(),
}
self.read_receipts.set_source(&event.read_receipts());
self.event.replace(Some(event));
}
}
}
glib::wrapper! {
pub struct StateRow(ObjectSubclass<imp::StateRow>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl StateRow {
pub fn new() -> Self {
glib::Object::new()
}
pub fn content(&self) -> &adw::Bin {
&self.imp().content
}
fn update_with_other_state(&self, event: &Event, other_state: &OtherState) {
let widget = match other_state.content() {
AnyOtherFullStateEventContent::RoomCreate(content) => {
WidgetType::Creation(StateCreation::new(content))
}
AnyOtherFullStateEventContent::RoomEncryption(_) => {
WidgetType::Text(gettext("This room is encrypted from this point on."))
}
AnyOtherFullStateEventContent::RoomThirdPartyInvite(content) => {
let display_name = match content {
FullStateEventContent::Original { content, .. } => {
match &content.display_name {
s if s.is_empty() => other_state.state_key(),
s => s,
}
}
FullStateEventContent::Redacted(_) => other_state.state_key(),
};
WidgetType::Text(gettext_f(
"{user} was invited to this room.",
&[("user", display_name)],
))
}
AnyOtherFullStateEventContent::RoomTombstone(_) => {
WidgetType::Tombstone(StateTombstone::new(&event.room()))
}
_ => {
warn!(
"Unsupported state event: {}",
other_state.content().event_type()
);
WidgetType::Text(gettext("An unsupported state event was received."))
}
};
let content = self.content();
match widget {
WidgetType::Text(message) => {
if let Some(child) = content.child().and_downcast::<gtk::Label>() {
child.set_text(&message);
} else {
content.set_child(Some(&text(message)));
};
}
WidgetType::Creation(widget) => content.set_child(Some(&widget)),
WidgetType::Tombstone(widget) => content.set_child(Some(&widget)),
}
}
fn update_with_membership_change(
&self,
membership_change: &RoomMembershipChange,
sender: &UserId,
) {
let display_name = match membership_change.content() {
FullStateEventContent::Original { content, .. } => content
.displayname
.clone()
.unwrap_or_else(|| membership_change.user_id().to_string()),
FullStateEventContent::Redacted(_) => membership_change.user_id().to_string(),
};
let supported_membership_change =
match membership_change.change().unwrap_or(MembershipChange::None) {
MembershipChange::Joined => MembershipChange::Joined,
MembershipChange::Left => MembershipChange::Left,
MembershipChange::Banned => MembershipChange::Banned,
MembershipChange::Unbanned => MembershipChange::Unbanned,
MembershipChange::Kicked => MembershipChange::Kicked,
MembershipChange::Invited => MembershipChange::Invited,
MembershipChange::KickedAndBanned => MembershipChange::KickedAndBanned,
MembershipChange::InvitationAccepted => MembershipChange::InvitationAccepted,
MembershipChange::InvitationRejected => MembershipChange::InvitationRejected,
MembershipChange::InvitationRevoked => MembershipChange::InvitationRevoked,
MembershipChange::Knocked => MembershipChange::Knocked,
MembershipChange::KnockAccepted => MembershipChange::KnockAccepted,
MembershipChange::KnockRetracted => MembershipChange::KnockRetracted,
MembershipChange::KnockDenied => MembershipChange::KnockDenied,
_ => {
let membership = match membership_change.content() {
FullStateEventContent::Original { content, .. } => &content.membership,
FullStateEventContent::Redacted(content) => &content.membership,
};
match membership {
MembershipState::Ban => MembershipChange::Banned,
MembershipState::Invite => MembershipChange::Invited,
MembershipState::Join => MembershipChange::Joined,
MembershipState::Knock => MembershipChange::Knocked,
MembershipState::Leave => {
if membership_change.user_id() == sender {
MembershipChange::Left
} else {
MembershipChange::Kicked
}
}
_ => MembershipChange::NotImplemented,
}
}
};
let message = match supported_membership_change {
MembershipChange::Joined => {
gettext_f("{user} joined this room.", &[("user", &display_name)])
}
MembershipChange::Left => {
gettext_f("{user} left the room.", &[("user", &display_name)])
}
MembershipChange::Banned => gettext_f(
"{user} was banned.",
&[("user", &display_name)],
),
MembershipChange::Unbanned => gettext_f(
"{user} was unbanned.",
&[("user", &display_name)],
),
MembershipChange::Kicked => gettext_f(
"{user} was kicked out of the room.",
&[("user", &display_name)],
),
MembershipChange::Invited | MembershipChange::KnockAccepted => gettext_f(
"{user} was invited to this room.",
&[("user", &display_name)],
),
MembershipChange::KickedAndBanned => gettext_f(
"{user} was kicked out of the room and banned.",
&[("user", &display_name)],
),
MembershipChange::InvitationAccepted => gettext_f(
"{user} accepted the invite.",
&[("user", &display_name)],
),
MembershipChange::InvitationRejected => gettext_f(
"{user} rejected the invite.",
&[("user", &display_name)],
),
MembershipChange::InvitationRevoked => gettext_f(
"The invitation for {user} has been revoked.",
&[("user", &display_name)],
),
MembershipChange::Knocked =>
{
gettext_f(
"{user} requested to be invited to this room.",
&[("user", &display_name)],
)
}
MembershipChange::KnockRetracted => gettext_f(
"{user} retracted their request to be invited to this room.",
&[("user", &display_name)],
),
MembershipChange::KnockDenied => gettext_f(
"{user}’s request to be invited to this room was denied.",
&[("user", &display_name)],
),
_ => {
warn!(
"Unsupported membership change event: {:?}",
membership_change.content()
);
gettext("An unsupported room member event was received.")
}
};
let content = self.content();
if let Some(child) = content.child().and_downcast::<gtk::Label>() {
child.set_text(&message);
} else {
content.set_child(Some(&text(message)));
};
}
fn update_with_profile_change(&self, profile_change: &MemberProfileChange, display_name: &str) {
let message = if let Some(displayname) = profile_change.displayname_change() {
if let Some(prev_name) = &displayname.old {
if let Some(new_name) = &displayname.new {
gettext_f(
"{previous_user_name} changed their display name to {new_user_name}.",
&[
("previous_user_name", prev_name),
("new_user_name", new_name),
],
)
} else {
gettext_f(
"{previous_user_name} removed their display name.",
&[("previous_user_name", prev_name)],
)
}
} else {
let new_name = displayname
.new
.as_ref()
.expect("At least one displayname is set in a display name change");
gettext_f(
"{user_id} set their display name to {new_user_name}.",
&[
("user_id", profile_change.user_id().as_ref()),
("new_user_name", new_name),
],
)
}
} else if let Some(avatar_url) = profile_change.avatar_url_change() {
if avatar_url.old.is_none() {
gettext_f(
"{user} set their avatar.",
&[("user", display_name)],
)
} else if avatar_url.new.is_none() {
gettext_f(
"{user} removed their avatar.",
&[("user", display_name)],
)
} else {
gettext_f(
"{user} changed their avatar.",
&[("user", display_name)],
)
}
} else {
gettext_f("{user} joined this room.", &[("user", display_name)])
};
let content = self.content();
if let Some(child) = content.child().and_downcast::<gtk::Label>() {
child.set_text(&message);
} else {
content.set_child(Some(&text(message)));
};
}
}
enum WidgetType {
Text(String),
Creation(StateCreation),
Tombstone(StateTombstone),
}
fn text(label: String) -> gtk::Label {
let child = gtk::Label::new(Some(&label));
child.set_css_classes(&["event-content", "dim-label"]);
child.set_wrap(true);
child.set_wrap_mode(gtk::pango::WrapMode::WordChar);
child.set_xalign(0.0);
child
}