room: Port to glib::Properties macro

This commit is contained in:
Kévin Commaille 2023-12-12 10:17:22 +01:00
parent a6d10c65e5
commit 15adbfecbe
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
28 changed files with 756 additions and 1097 deletions

View File

@ -1,4 +1,4 @@
use std::{borrow::Cow, fmt};
use std::{borrow::Cow, fmt, ops::Deref};
use gtk::{gio, glib, prelude::*, subclass::prelude::*};
use indexmap::IndexMap;
@ -82,7 +82,15 @@ pub enum MessageState {
#[derive(Clone, Debug, glib::Boxed)]
#[boxed_type(name = "BoxedEventTimelineItem")]
pub struct BoxedEventTimelineItem(EventTimelineItem);
pub struct BoxedEventTimelineItem(pub EventTimelineItem);
impl Deref for BoxedEventTimelineItem {
type Target = EventTimelineItem;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// A user's read receipt.
#[derive(Clone, Debug)]
@ -92,29 +100,47 @@ pub struct UserReadReceipt {
}
mod imp {
use std::cell::{Cell, RefCell};
use glib::object::WeakRef;
use once_cell::sync::Lazy;
use std::{
cell::{Cell, RefCell},
marker::PhantomData,
};
use super::*;
#[derive(Debug)]
#[derive(Debug, glib::Properties)]
#[properties(wrapper_type = super::Event)]
pub struct Event {
/// The underlying SDK timeline item.
pub item: RefCell<Option<EventTimelineItem>>,
#[property(get = Self::item, set = Self::set_item, type = BoxedEventTimelineItem)]
pub item: RefCell<Option<BoxedEventTimelineItem>>,
/// The room containing this `Event`.
pub room: WeakRef<Room>,
#[property(get, set = Self::set_room, construct_only)]
pub room: glib::WeakRef<Room>,
/// The reactions on this event.
#[property(get)]
pub reactions: ReactionList,
/// The read receipts on this event.
#[property(get)]
pub read_receipts: gio::ListStore,
/// The state of this event.
#[property(get, builder(MessageState::default()))]
pub state: Cell<MessageState>,
/// The pretty-formatted JSON source for this `Event`, if it has
/// been echoed back by the server.
#[property(get = Self::source)]
pub source: PhantomData<Option<String>>,
/// The timestamp of this `Event`.
#[property(get = Self::timestamp)]
pub timestamp: PhantomData<glib::DateTime>,
/// Whether this `Event` was edited.
#[property(get = Self::is_edited)]
pub is_edited: PhantomData<bool>,
/// Whether this `Event` should be highlighted.
#[property(get = Self::is_highlighted)]
pub is_highlighted: PhantomData<bool>,
/// Whether this event has any read receipt.
#[property(get = Self::has_read_receipts)]
pub has_read_receipts: PhantomData<bool>,
}
impl Default for Event {
@ -125,6 +151,11 @@ mod imp {
reactions: Default::default(),
read_receipts: gio::ListStore::new::<glib::BoxedAnyObject>(),
state: Default::default(),
source: Default::default(),
timestamp: Default::default(),
is_edited: Default::default(),
is_highlighted: Default::default(),
has_read_receipts: Default::default(),
}
}
}
@ -136,76 +167,8 @@ mod imp {
type ParentType = TimelineItem;
}
impl ObjectImpl for Event {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecBoxed::builder::<BoxedEventTimelineItem>("item")
.write_only()
.build(),
glib::ParamSpecString::builder("source").read_only().build(),
glib::ParamSpecObject::builder::<Room>("room")
.construct_only()
.build(),
glib::ParamSpecBoxed::builder::<glib::DateTime>("timestamp")
.read_only()
.build(),
glib::ParamSpecObject::builder::<ReactionList>("reactions")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("is-edited")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("is-highlighted")
.read_only()
.build(),
glib::ParamSpecObject::builder::<gio::ListStore>("read-receipts")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("has-read-receipts")
.read_only()
.build(),
glib::ParamSpecEnum::builder::<MessageState>("state")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"item" => {
let item = value.get::<BoxedEventTimelineItem>().unwrap();
obj.set_item(item.0);
}
"room" => {
obj.set_room(value.get().unwrap());
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"source" => obj.source().to_value(),
"room" => obj.room().to_value(),
"timestamp" => obj.timestamp().to_value(),
"reactions" => obj.reactions().to_value(),
"is-edited" => obj.is_edited().to_value(),
"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!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for Event {}
impl TimelineItemImpl for Event {
fn id(&self) -> String {
@ -239,6 +202,96 @@ mod imp {
true
}
}
impl Event {
/// The underlying SDK timeline item of this `Event`.
fn item(&self) -> BoxedEventTimelineItem {
self.item.borrow().clone().unwrap()
}
/// Set the underlying SDK timeline item of this `Event`.
fn set_item(&self, item: BoxedEventTimelineItem) {
let obj = self.obj();
let was_edited = self.is_edited();
let was_highlighted = self.is_highlighted();
self.reactions.update(item.reactions().clone());
obj.update_read_receipts(item.read_receipts());
self.item.replace(Some(item));
obj.notify_source();
if self.is_edited() != was_edited {
obj.notify_is_edited();
}
if self.is_highlighted() != was_highlighted {
obj.notify_is_highlighted();
}
obj.update_state();
}
/// The pretty-formatted JSON source for this `Event`, if it has
/// been echoed back by the server.
fn source(&self) -> Option<String> {
self.item
.borrow()
.as_ref()
.unwrap()
.original_json()
.map(|raw| {
// We have to convert it to a Value, because a RawValue cannot be
// pretty-printed.
let json = serde_json::to_value(raw).unwrap();
serde_json::to_string_pretty(&json).unwrap()
})
}
/// Set the room that contains this `Event`.
fn set_room(&self, room: Room) {
self.room.set(Some(&room));
if let Some(session) = room.session() {
self.reactions.set_user(session.user().clone());
}
}
/// The timestamp of this `Event`.
fn timestamp(&self) -> glib::DateTime {
let ts = self.obj().origin_server_ts();
glib::DateTime::from_unix_utc(ts.as_secs().into())
.and_then(|t| t.to_local())
.unwrap()
}
/// Whether this `Event` was edited.
fn is_edited(&self) -> bool {
let item_ref = self.item.borrow();
let Some(item) = item_ref.as_ref() else {
return false;
};
match item.content() {
TimelineItemContent::Message(msg) => msg.is_edited(),
_ => false,
}
}
/// Whether this `Event` should be highlighted.
fn is_highlighted(&self) -> bool {
let item_ref = self.item.borrow();
let Some(item) = item_ref.as_ref() else {
return false;
};
item.is_highlighted()
}
/// Whether this event has any read receipt.
fn has_read_receipts(&self) -> bool {
self.read_receipts.n_items() > 0
}
}
}
glib::wrapper! {
@ -264,13 +317,13 @@ impl Event {
EventKey::TransactionId(txn_id)
if item.is_local_echo() && item.transaction_id() == Some(txn_id) =>
{
self.set_item(item.clone());
self.set_item(BoxedEventTimelineItem(item.clone()));
return true;
}
EventKey::EventId(event_id)
if !item.is_local_echo() && item.event_id() == Some(event_id) =>
{
self.set_item(item.clone());
self.set_item(BoxedEventTimelineItem(item.clone()));
return true;
}
_ => {}
@ -279,43 +332,6 @@ impl Event {
false
}
/// The room that contains this `Event`.
pub fn room(&self) -> Room {
self.imp().room.upgrade().unwrap()
}
/// Set the room that contains this `Event`.
fn set_room(&self, room: Room) {
let imp = self.imp();
imp.room.set(Some(&room));
imp.reactions.set_user(room.session().user());
}
/// The underlying SDK timeline item of this `Event`.
pub fn item(&self) -> EventTimelineItem {
self.imp().item.borrow().clone().unwrap()
}
/// Set the underlying SDK timeline item of this `Event`.
pub fn set_item(&self, item: EventTimelineItem) {
let was_edited = self.is_edited();
let was_highlighted = self.is_highlighted();
let imp = self.imp();
imp.reactions.update(item.reactions().clone());
self.update_read_receipts(item.read_receipts());
imp.item.replace(Some(item));
self.notify("source");
if self.is_edited() != was_edited {
self.notify("is-edited");
}
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
/// by the server.
pub fn raw(&self) -> Option<Raw<AnySyncTimelineEvent>> {
@ -328,24 +344,6 @@ impl Event {
.cloned()
}
/// The pretty-formatted JSON source for this `Event`, if it has
/// been echoed back by the server.
pub fn source(&self) -> Option<String> {
self.imp()
.item
.borrow()
.as_ref()
.unwrap()
.original_json()
.map(|raw| {
// We have to convert it to a Value, because a RawValue cannot be
// pretty-printed.
let json = serde_json::to_value(raw).unwrap();
serde_json::to_string_pretty(&json).unwrap()
})
}
/// The unique of this `Event` in the timeline.
pub fn key(&self) -> EventKey {
let item_ref = self.imp().item.borrow();
@ -390,6 +388,7 @@ impl Event {
/// available, otherwise it will be created on every call.
pub fn sender(&self) -> Member {
self.room()
.unwrap()
.get_or_create_members()
.get_or_create(self.sender_id())
}
@ -410,15 +409,6 @@ impl Event {
self.origin_server_ts().get().into()
}
/// The timestamp of this `Event`.
pub fn timestamp(&self) -> glib::DateTime {
let ts = self.origin_server_ts();
glib::DateTime::from_unix_utc(ts.as_secs().into())
.and_then(|t| t.to_local())
.unwrap()
}
/// Whether this `Event` is redacted.
pub fn is_redacted(&self) -> bool {
matches!(
@ -440,24 +430,6 @@ impl Event {
}
}
/// Whether this `Event` was edited.
pub fn is_edited(&self) -> bool {
let item_ref = self.imp().item.borrow();
let Some(item) = item_ref.as_ref() else {
return false;
};
match item.content() {
TimelineItemContent::Message(msg) => msg.is_edited(),
_ => false,
}
}
/// 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();
@ -495,32 +467,7 @@ impl Event {
}
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();
let Some(item) = item_ref.as_ref() else {
return false;
};
item.is_highlighted()
}
/// The reactions to this event.
pub fn reactions(&self) -> &ReactionList {
&self.imp().reactions
}
/// The read receipts on this event.
pub fn read_receipts(&self) -> &gio::ListStore {
&self.imp().read_receipts
}
/// Whether this event has any read receipt.
pub fn has_read_receipts(&self) -> bool {
self.imp().read_receipts.n_items() > 0
self.notify_state();
}
/// Update the read receipts list with the given receipts.
@ -566,7 +513,7 @@ impl Event {
let has_read_receipts = new_count > 0;
if had_read_receipts != has_read_receipts {
self.notify("has-read-receipts");
self.notify_has_read_receipts();
}
}
@ -599,11 +546,14 @@ impl Event {
///
/// This is a no-op if called for a local event.
pub async fn fetch_missing_details(&self) -> Result<(), TimelineError> {
let Some(room) = self.room() else {
return Ok(());
};
let Some(event_id) = self.event_id() else {
return Ok(());
};
let timeline = self.room().timeline().matrix_timeline();
let timeline = room.timeline().matrix_timeline();
spawn_tokio!(async move { timeline.fetch_details_for_event(&event_id).await })
.await
.unwrap()
@ -623,11 +573,21 @@ impl Event {
/// Returns `Err` if an error occurred while fetching the content. Panics on
/// an incompatible event.
pub async fn get_media_content(&self) -> Result<(String, Vec<u8>), matrix_sdk::Error> {
let Some(room) = self.room() else {
return Err(matrix_sdk::Error::UnknownError(
"Failed to upgrade Room".into(),
));
};
let Some(session) = room.session() else {
return Err(matrix_sdk::Error::UnknownError(
"Failed to upgrade Session".into(),
));
};
let TimelineItemContent::Message(message) = self.content() else {
panic!("Trying to get the media content of an event of incompatible type");
};
let client = self.room().session().client();
let client = session.client();
get_media_content(client, message.msgtype().clone()).await
}
@ -648,13 +608,6 @@ impl Event {
pub fn counts_as_unread(&self) -> bool {
count_as_unread(self.imp().item.borrow().as_ref().unwrap().content())
}
/// Listen to changes of the source of this `TimelineEvent`.
pub fn connect_source_notify<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_notify_local(Some("source"), move |this, _| {
f(this);
})
}
}
/// Whether the given event can count as an unread message.

View File

@ -5,22 +5,30 @@ use super::EventKey;
use crate::{prelude::*, session::model::User};
mod imp {
use std::cell::RefCell;
use once_cell::{sync::Lazy, unsync::OnceCell};
use std::{
cell::{OnceCell, RefCell},
marker::PhantomData,
};
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::ReactionGroup)]
pub struct ReactionGroup {
/// The user of the parent session.
#[property(get, construct_only)]
pub user: OnceCell<User>,
/// The key of the group.
#[property(get, construct_only)]
pub key: OnceCell<String>,
/// The reactions in the group.
pub reactions: RefCell<Option<SdkReactionGroup>>,
/// The number of reactions in this group.
#[property(get = Self::count)]
pub count: PhantomData<u32>,
/// Whether this group has a reaction from our own user.
#[property(get = Self::has_user)]
pub has_user: PhantomData<bool>,
}
#[glib::object_subclass]
@ -30,50 +38,8 @@ mod imp {
type Interfaces = (gio::ListModel,);
}
impl ObjectImpl for ReactionGroup {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<User>("user")
.construct_only()
.build(),
glib::ParamSpecString::builder("key")
.construct_only()
.build(),
glib::ParamSpecUInt::builder("count").read_only().build(),
glib::ParamSpecBoolean::builder("has-user")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"user" => {
self.user.set(value.get().unwrap()).unwrap();
}
"key" => {
self.key.set(value.get().unwrap()).unwrap();
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"user" => obj.user().to_value(),
"key" => obj.key().to_value(),
"count" => obj.count().to_value(),
"has-user" => obj.has_user().to_value(),
_ => unimplemented!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for ReactionGroup {}
impl ListModelImpl for ReactionGroup {
fn item_type(&self) -> glib::Type {
@ -97,6 +63,23 @@ mod imp {
})
}
}
impl ReactionGroup {
/// The number of reactions in this group.
fn count(&self) -> u32 {
self.n_items()
}
/// Whether this group has a reaction from our own user.
fn has_user(&self) -> bool {
let user_id = UserExt::user_id(self.user.get().unwrap());
self.reactions
.borrow()
.as_ref()
.filter(|reactions| reactions.by_sender(&user_id).next().is_some())
.is_some()
}
}
}
glib::wrapper! {
@ -113,25 +96,10 @@ impl ReactionGroup {
.build()
}
/// The user of the parent session.
pub fn user(&self) -> &User {
self.imp().user.get().unwrap()
}
/// The key of the group.
pub fn key(&self) -> &str {
self.imp().key.get().unwrap()
}
/// The number of reactions in this group
pub fn count(&self) -> u32 {
self.imp().n_items()
}
/// The event ID of the reaction in this group sent by the logged-in user,
/// if any.
pub fn user_reaction_event_key(&self) -> Option<EventKey> {
let user_id = UserExt::user_id(self.user());
let user_id = UserExt::user_id(&self.user());
self.imp()
.reactions
.borrow()
@ -148,17 +116,6 @@ impl ReactionGroup {
})
}
/// Whether this group has a reaction from the logged-in user.
pub fn has_user(&self) -> bool {
let user_id = UserExt::user_id(self.user());
self.imp()
.reactions
.borrow()
.as_ref()
.filter(|reactions| reactions.by_sender(&user_id).next().is_some())
.is_some()
}
/// Update this group with the given reactions.
pub fn update(&self, new_reactions: SdkReactionGroup) {
let prev_has_user = self.has_user();
@ -188,11 +145,11 @@ impl ReactionGroup {
self.items_changed(0, prev_count, new_count);
if self.count() != prev_count {
self.notify("count");
self.notify_count();
}
if self.has_user() != prev_has_user {
self.notify("has-user");
self.notify_has_user();
}
}
}

View File

@ -53,15 +53,19 @@ impl From<MembershipState> for Membership {
mod imp {
use std::cell::Cell;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::Member)]
pub struct Member {
/// The power level of the member.
#[property(get, minimum = POWER_LEVEL_MIN, maximum = POWER_LEVEL_MAX)]
pub power_level: Cell<PowerLevel>,
/// This member's membership state.
#[property(get, builder(Membership::default()))]
pub membership: Cell<Membership>,
/// The timestamp of the latest activity of this member.
#[property(get, set = Self::set_latest_activity, explicit_notify)]
pub latest_activity: Cell<u64>,
}
@ -72,43 +76,18 @@ mod imp {
type ParentType = User;
}
impl ObjectImpl for Member {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecInt64::builder("power-level")
.minimum(POWER_LEVEL_MIN)
.maximum(POWER_LEVEL_MAX)
.read_only()
.build(),
glib::ParamSpecEnum::builder::<Membership>("membership")
.read_only()
.build(),
glib::ParamSpecUInt64::builder("latest-activity")
.explicit_notify()
.build(),
]
});
#[glib::derived_properties]
impl ObjectImpl for Member {}
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"latest-activity" => self.obj().set_latest_activity(value.get().unwrap()),
_ => unimplemented!(),
impl Member {
/// Set the timestamp of the latest activity of this member.
fn set_latest_activity(&self, activity: u64) {
if self.latest_activity.get() >= activity {
return;
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"power-level" => obj.power_level().to_value(),
"membership" => obj.membership().to_value(),
"latest-activity" => obj.latest_activity().to_value(),
_ => unimplemented!(),
}
self.latest_activity.set(activity);
self.obj().notify_latest_activity();
}
}
}
@ -127,18 +106,13 @@ impl Member {
.build()
}
/// The power level of the member.
pub fn power_level(&self) -> PowerLevel {
self.imp().power_level.get()
}
/// Set the power level of the member.
fn set_power_level(&self, power_level: PowerLevel) {
if self.power_level() == power_level {
return;
}
self.imp().power_level.replace(power_level);
self.notify("power-level");
self.notify_power_level();
}
pub fn role(&self) -> MemberRole {
@ -157,12 +131,6 @@ impl Member {
self.role().is_peasant()
}
/// This member's membership state.
pub fn membership(&self) -> Membership {
let imp = self.imp();
imp.membership.get()
}
/// Set this member's membership state.
fn set_membership(&self, membership: Membership) {
if self.membership() == membership {
@ -170,22 +138,7 @@ impl Member {
}
let imp = self.imp();
imp.membership.replace(membership);
self.notify("membership");
}
/// The timestamp of the latest activity of this member.
pub fn latest_activity(&self) -> u64 {
self.imp().latest_activity.get()
}
/// Set the timestamp of the latest activity of this member.
pub fn set_latest_activity(&self, activity: u64) {
if self.latest_activity() >= activity {
return;
}
self.imp().latest_activity.set(activity);
self.notify("latest-activity");
self.notify_membership();
}
/// Update the user based on the room member.

View File

@ -20,18 +20,18 @@ use crate::{spawn, spawn_tokio, utils::LoadingState};
mod imp {
use std::cell::{Cell, RefCell};
use glib::object::WeakRef;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::MemberList)]
pub struct MemberList {
/// The list of known members.
pub members: RefCell<IndexMap<OwnedUserId, Member>>,
/// The room these members belong to.
pub room: WeakRef<Room>,
#[property(get, set = Self::set_room, construct_only)]
pub room: glib::WeakRef<Room>,
/// The loading state of the list.
#[property(get, builder(LoadingState::default()))]
pub state: Cell<LoadingState>,
}
@ -42,39 +42,8 @@ mod imp {
type Interfaces = (gio::ListModel,);
}
impl ObjectImpl for MemberList {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<Room>("room")
.construct_only()
.build(),
glib::ParamSpecEnum::builder::<LoadingState>("state")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"room" => self.obj().set_room(&value.get().ok().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"room" => obj.room().to_value(),
"state" => obj.state().to_value(),
_ => unimplemented!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for MemberList {}
impl ListModelImpl for MemberList {
fn item_type(&self) -> glib::Type {
@ -93,6 +62,22 @@ mod imp {
.map(|(_user_id, member)| member.clone().upcast())
}
}
impl MemberList {
/// Set the room these members belong to.
fn set_room(&self, room: Room) {
let obj = self.obj();
self.room.set(Some(&room));
obj.notify_room();
spawn!(
glib::Priority::LOW,
clone!(@weak obj => async move {
obj.load().await;
})
);
}
}
}
glib::wrapper! {
@ -108,28 +93,6 @@ impl MemberList {
glib::Object::builder().property("room", room).build()
}
/// The room containing these members.
pub fn room(&self) -> Room {
self.imp().room.upgrade().unwrap()
}
fn set_room(&self, room: &Room) {
self.imp().room.set(Some(room));
self.notify("room");
spawn!(
glib::Priority::LOW,
clone!(@weak self as obj => async move {
obj.load().await;
})
);
}
/// The state of this list.
pub fn state(&self) -> LoadingState {
self.imp().state.get()
}
/// Set whether this list is being loaded.
fn set_state(&self, state: LoadingState) {
if self.state() == state {
@ -137,7 +100,7 @@ impl MemberList {
}
self.imp().state.set(state);
self.notify("state");
self.notify_state();
}
pub fn reload(&self) {
@ -150,13 +113,15 @@ impl MemberList {
/// Load this list.
async fn load(&self) {
let Some(room) = self.room() else {
return;
};
if matches!(self.state(), LoadingState::Loading | LoadingState::Ready) {
return;
}
self.set_state(LoadingState::Loading);
let room = self.room();
let matrix_room = room.matrix_room();
// First load what we have locally.
@ -210,12 +175,15 @@ impl MemberList {
/// If some of the values do not correspond to existing members, new members
/// are created.
fn update_from_room_members(&self, new_members: &[matrix_sdk::room::RoomMember]) {
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
let mut members = imp.members.borrow_mut();
let prev_len = members.len();
for member in new_members {
if let Entry::Vacant(entry) = members.entry(member.user_id().into()) {
entry.insert(Member::new(&self.room(), member.user_id()));
entry.insert(Member::new(&room, member.user_id()));
}
}
let num_members_added = members.len().saturating_sub(prev_len);
@ -234,7 +202,7 @@ impl MemberList {
}
// Restore the members activity according to the known timeline events.
for item in self.room().timeline().items().iter::<glib::Object>().rev() {
for item in room.timeline().items().iter::<glib::Object>().rev() {
let Ok(item) = item else {
// The iterator is broken, stop.
break;
@ -270,7 +238,7 @@ impl MemberList {
.entry(user_id)
.or_insert_with_key(|user_id| {
was_member_added = true;
Member::new(&self.room(), user_id)
Member::new(&self.room().unwrap(), user_id)
})
.clone();

View File

@ -56,28 +56,55 @@ use super::{
use crate::{components::Pill, gettext_f, prelude::*, spawn, spawn_tokio};
mod imp {
use std::cell::Cell;
use std::{
cell::{Cell, OnceCell},
marker::PhantomData,
};
use glib::{object::WeakRef, subclass::Signal};
use once_cell::{sync::Lazy, unsync::OnceCell};
use glib::subclass::Signal;
use once_cell::sync::Lazy;
use super::*;
#[derive(Default)]
#[derive(Default, glib::Properties)]
#[properties(wrapper_type = super::Room)]
pub struct Room {
/// The ID of this room.
#[property(set = Self::set_room_id, construct_only, type = String)]
pub room_id: OnceCell<OwnedRoomId>,
pub matrix_room: RefCell<Option<MatrixRoom>>,
pub session: WeakRef<Session>,
pub name: RefCell<Option<String>>,
/// The current session.
#[property(get, construct_only)]
pub session: glib::WeakRef<Session>,
/// The name that is set for this room.
///
/// This can be empty, the display name should be used instead in the
/// interface.
#[property(get = Self::name)]
pub name: PhantomData<Option<String>>,
/// The display name of this room.
#[property(get = Self::display_name, type = String)]
pub display_name: RefCell<Option<String>>,
/// The Avatar data of this room.
#[property(get)]
pub avatar_data: OnceCell<AvatarData>,
/// The category of this room.
#[property(get, builder(RoomType::default()))]
pub category: Cell<RoomType>,
/// The timeline of this room.
#[property(get)]
pub timeline: OnceCell<Timeline>,
pub members: WeakRef<MemberList>,
/// The members of this room.
#[property(get)]
pub members: glib::WeakRef<MemberList>,
/// The number of joined members in the room, according to the
/// homeserver.
#[property(get)]
pub joined_members_count: Cell<u64>,
/// The user who sent the invite to this room. This is only set when
/// this room is an invitation.
/// The user who sent the invite to this room.
///
/// This is only set when this room is an invitation.
#[property(get)]
pub inviter: RefCell<Option<Member>>,
pub power_levels: RefCell<PowerLevels>,
/// The timestamp of the room's latest activity.
@ -86,27 +113,46 @@ mod imp {
/// unread.
///
/// If it is not known, it will return `0`.
#[property(get)]
pub latest_activity: Cell<u64>,
/// Whether all messages of this room are read.
#[property(get)]
pub is_read: Cell<bool>,
/// The highlight state of the room,
/// The highlight state of the room.
#[property(get)]
pub highlight: Cell<HighlightFlags>,
/// The ID of the room that was upgraded and that this one replaces.
pub predecessor_id: OnceCell<OwnedRoomId>,
/// The ID of the successor of this Room, if this room was upgraded.
pub successor_id: OnceCell<OwnedRoomId>,
/// The successor of this Room, if this room was upgraded.
pub successor: WeakRef<super::Room>,
/// The successor of this Room, if this room was upgraded and the
/// successor was joined.
#[property(get)]
pub successor: glib::WeakRef<super::Room>,
/// The most recent verification request event.
#[property(get, set)]
pub verification: RefCell<Option<IdentityVerification>>,
/// Whether this room is encrypted
pub is_encrypted: Cell<bool>,
/// Whether this room is encrypted.
#[property(get)]
pub encrypted: Cell<bool>,
/// The list of members currently typing in this room.
#[property(get)]
pub typing_list: TypingList,
/// Whether anyone can join this room.
#[property(get)]
pub is_join_rule_public: Cell<bool>,
/// Whether this room is a DM.
/// Whether this room is a direct chat.
#[property(get)]
pub is_direct: Cell<bool>,
/// The number of unread notifications of this room.
#[property(get = Self::notification_count)]
pub notification_count: PhantomData<u64>,
/// The topic of this room.
#[property(get = Self::topic)]
pub topic: PhantomData<Option<String>>,
/// Whether this room has been upgraded.
#[property(get = Self::is_tombstoned)]
pub is_tombstoned: PhantomData<bool>,
}
#[glib::object_subclass]
@ -116,131 +162,8 @@ mod imp {
type ParentType = SidebarItem;
}
#[glib::derived_properties]
impl ObjectImpl for Room {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::builder("room-id")
.construct_only()
.build(),
glib::ParamSpecObject::builder::<Session>("session")
.construct_only()
.build(),
glib::ParamSpecString::builder("name").read_only().build(),
glib::ParamSpecString::builder("display-name")
.read_only()
.build(),
glib::ParamSpecObject::builder::<Member>("inviter")
.read_only()
.build(),
glib::ParamSpecObject::builder::<AvatarData>("avatar-data")
.read_only()
.build(),
glib::ParamSpecObject::builder::<Timeline>("timeline")
.read_only()
.build(),
glib::ParamSpecFlags::builder::<HighlightFlags>("highlight")
.read_only()
.build(),
glib::ParamSpecUInt64::builder("notification-count")
.read_only()
.build(),
glib::ParamSpecEnum::builder::<RoomType>("category")
.read_only()
.build(),
glib::ParamSpecString::builder("topic").read_only().build(),
glib::ParamSpecUInt64::builder("latest-activity")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("is-read")
.read_only()
.build(),
glib::ParamSpecObject::builder::<MemberList>("members")
.read_only()
.build(),
glib::ParamSpecUInt64::builder("joined-members-count")
.read_only()
.build(),
glib::ParamSpecString::builder("predecessor-id")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("is-tombstoned")
.read_only()
.build(),
glib::ParamSpecString::builder("successor-id")
.read_only()
.build(),
glib::ParamSpecObject::builder::<super::Room>("successor")
.read_only()
.build(),
glib::ParamSpecObject::builder::<IdentityVerification>("verification")
.explicit_notify()
.build(),
glib::ParamSpecBoolean::builder("encrypted")
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<TypingList>("typing-list")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("is-join-rule-public")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("is-direct")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"session" => self.session.set(value.get().ok().as_ref()),
"room-id" => self
.room_id
.set(RoomId::parse(value.get::<&str>().unwrap()).unwrap())
.unwrap(),
"verification" => obj.set_verification(value.get().unwrap()),
"encrypted" => obj.set_is_encrypted(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"room-id" => obj.room_id().as_str().to_value(),
"session" => obj.session().to_value(),
"inviter" => obj.inviter().to_value(),
"name" => obj.name().to_value(),
"display-name" => obj.display_name().to_value(),
"avatar-data" => obj.avatar_data().to_value(),
"timeline" => self.timeline.get().unwrap().to_value(),
"category" => obj.category().to_value(),
"highlight" => obj.highlight().to_value(),
"topic" => obj.topic().to_value(),
"members" => obj.members().to_value(),
"joined-members-count" => obj.joined_members_count().to_value(),
"notification-count" => obj.notification_count().to_value(),
"latest-activity" => obj.latest_activity().to_value(),
"is-read" => obj.is_read().to_value(),
"predecessor-id" => obj.predecessor_id().map(|id| id.as_str()).to_value(),
"is-tombstoned" => obj.is_tombstoned().to_value(),
"successor-id" => obj.successor_id().map(|id| id.as_str()).to_value(),
"successor" => obj.successor().to_value(),
"verification" => obj.verification().to_value(),
"encrypted" => obj.is_encrypted().to_value(),
"typing-list" => obj.typing_list().to_value(),
"is-join-rule-public" => obj.is_join_rule_public().to_value(),
"is-direct" => obj.is_direct().to_value(),
_ => unimplemented!(),
}
}
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("room-forgotten").build()]);
@ -250,8 +173,11 @@ mod imp {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
let Some(session) = obj.session() else {
return;
};
obj.set_matrix_room(obj.session().client().get_room(obj.room_id()).unwrap());
obj.set_matrix_room(session.client().get_room(obj.room_id()).unwrap());
self.timeline.set(Timeline::new(&obj)).unwrap();
self.timeline
@ -265,7 +191,7 @@ mod imp {
// Initialize the avatar first since loading is async.
self.avatar_data
.set(AvatarData::with_image(AvatarImage::new(
&obj.session(),
&session,
obj.matrix_room().avatar_url().as_deref(),
AvatarUriSource::Room,
)))
@ -280,7 +206,7 @@ mod imp {
obj.setup_is_encrypted().await;
}));
obj.bind_property("display-name", obj.avatar_data(), "display-name")
obj.bind_property("display-name", &obj.avatar_data(), "display-name")
.sync_create()
.build();
@ -297,6 +223,55 @@ mod imp {
}
impl SidebarItemImpl for Room {}
impl Room {
/// Set the ID of this room.
fn set_room_id(&self, room_id: String) {
self.room_id.set(RoomId::parse(room_id).unwrap()).unwrap();
}
/// The name of this room.
///
/// This can be empty, the display name should be used instead in the
/// interface.
fn name(&self) -> Option<String> {
self.matrix_room.borrow().as_ref().unwrap().name()
}
/// The display name of this room.
pub fn display_name(&self) -> String {
let display_name = self.display_name.borrow().clone();
// Translators: This is displayed when the room name is unknown yet.
display_name.unwrap_or_else(|| gettext("Unknown"))
}
/// The number of unread notifications of this room.
fn notification_count(&self) -> u64 {
self.matrix_room
.borrow()
.as_ref()
.unwrap()
.unread_notification_counts()
.notification_count
}
/// The topic of this room.
fn topic(&self) -> Option<String> {
self.matrix_room
.borrow()
.as_ref()
.unwrap()
.topic()
.filter(|topic| {
!topic.is_empty() && topic.find(|c: char| !c.is_whitespace()).is_some()
})
}
/// Whether this room was tombstoned.
pub fn is_tombstoned(&self) -> bool {
self.matrix_room.borrow().as_ref().unwrap().is_tombstoned()
}
}
}
glib::wrapper! {
@ -327,11 +302,6 @@ impl Room {
this
}
/// The current session.
pub fn session(&self) -> Session {
self.imp().session.upgrade().unwrap()
}
/// The ID of this room.
pub fn room_id(&self) -> &RoomId {
self.imp().room_id.get().unwrap()
@ -375,11 +345,6 @@ impl Room {
self.matrix_room().state()
}
/// Whether this room is direct or not.
pub fn is_direct(&self) -> bool {
self.imp().is_direct.get()
}
/// Set whether this room is direct.
fn set_is_direct(&self, is_direct: bool) {
if self.is_direct() == is_direct {
@ -387,7 +352,7 @@ impl Room {
}
self.imp().is_direct.set(is_direct);
self.notify("is-direct");
self.notify_is_direct();
}
pub async fn load_is_direct(&self) {
@ -440,10 +405,6 @@ impl Room {
)
}
pub fn category(&self) -> RoomType {
self.imp().category.get()
}
fn set_category_internal(&self, category: RoomType) {
let old_category = self.category();
@ -452,7 +413,7 @@ impl Room {
}
self.imp().category.set(category);
self.notify("category");
self.notify_category();
}
/// Set the category of this room.
@ -699,10 +660,6 @@ impl Room {
self.set_joined_members_count(room_info.joined_members_count());
}
pub fn typing_list(&self) -> &TypingList {
&self.imp().typing_list
}
fn setup_typing(&self) {
let matrix_room = self.matrix_room();
if matrix_room.state() != RoomState::Joined {
@ -746,7 +703,9 @@ impl Room {
}
fn handle_receipt_event(&self, content: ReceiptEventContent) {
let session = self.session();
let Some(session) = self.session() else {
return;
};
let own_user_id = session.user_id();
for (_event_id, receipts) in content.iter() {
@ -759,6 +718,9 @@ impl Room {
}
fn handle_typing_event(&self, content: TypingEventContent) {
let Some(session) = self.session() else {
return;
};
let typing_list = &self.imp().typing_list;
let Some(members) = self.members() else {
@ -768,7 +730,6 @@ impl Room {
return;
};
let session = self.session();
let own_user_id = session.user_id();
let members = content
@ -780,11 +741,6 @@ impl Room {
typing_list.update(members);
}
/// The timeline of this room.
pub fn timeline(&self) -> &Timeline {
self.imp().timeline.get().unwrap()
}
/// The members of this room.
///
/// This creates the [`MemberList`] if no strong reference to it exists.
@ -795,21 +751,11 @@ impl Room {
} else {
let list = MemberList::new(self);
members.set(Some(&list));
self.notify("members");
self.notify_members();
list
}
}
/// The members of this room, if a strong reference to the list exists.
pub fn members(&self) -> Option<MemberList> {
self.imp().members.upgrade()
}
/// The number of joined members in the room, according to the homeserver.
pub fn joined_members_count(&self) -> u64 {
self.imp().joined_members_count.get()
}
/// Set the number of joined members in the room, according to the
/// homeserver.
fn set_joined_members_count(&self, count: u64) {
@ -818,11 +764,7 @@ impl Room {
}
self.imp().joined_members_count.set(count);
self.notify("joined-members-count");
}
fn notify_notification_count(&self) {
self.notify("notification-count");
self.notify_joined_members_count();
}
fn update_highlight(&self) {
@ -851,11 +793,6 @@ impl Room {
self.set_highlight(highlight);
}
/// How this room is highlighted.
pub fn highlight(&self) -> HighlightFlags {
self.imp().highlight.get()
}
/// Set how this room is highlighted.
fn set_highlight(&self, highlight: HighlightFlags) {
if self.highlight() == highlight {
@ -863,7 +800,7 @@ impl Room {
}
self.imp().highlight.set(highlight);
self.notify("highlight");
self.notify_highlight();
}
fn update_is_read(&self) {
@ -876,11 +813,6 @@ impl Room {
}));
}
/// Whether all messages of this room are read.
pub fn is_read(&self) -> bool {
self.imp().is_read.get()
}
/// Set whether all messages of this room are read.
pub fn set_is_read(&self, is_read: bool) {
if is_read == self.is_read() {
@ -888,22 +820,7 @@ impl Room {
}
self.imp().is_read.set(is_read);
self.notify("is-read");
}
/// The name of this room.
///
/// This can be empty, the display name should be used instead in the
/// interface.
pub fn name(&self) -> Option<String> {
self.matrix_room().name()
}
/// The display name of this room.
pub fn display_name(&self) -> String {
let display_name = self.imp().name.borrow().clone();
// Translators: This is displayed when the room name is unknown yet.
display_name.unwrap_or_else(|| gettext("Unknown"))
self.notify_is_read();
}
/// Set the display name of this room.
@ -912,8 +829,8 @@ impl Room {
return;
}
self.imp().name.replace(display_name);
self.notify("display-name");
self.imp().display_name.replace(display_name);
self.notify_display_name();
}
fn load_display_name(&self) {
@ -943,48 +860,23 @@ impl Room {
);
}
/// The number of unread notifications of this room.
pub fn notification_count(&self) -> u64 {
let matrix_room = self.imp().matrix_room.borrow();
matrix_room
.as_ref()
.unwrap()
.unread_notification_counts()
.notification_count
}
/// The Avatar of this room.
pub fn avatar_data(&self) -> &AvatarData {
self.imp().avatar_data.get().unwrap()
}
/// The topic of this room.
pub fn topic(&self) -> Option<String> {
self.matrix_room()
.topic()
.filter(|topic| !topic.is_empty() && topic.find(|c: char| !c.is_whitespace()).is_some())
}
pub fn power_levels(&self) -> PowerLevels {
self.imp().power_levels.borrow().clone()
}
/// The user who sent the invite to this room.
///
/// This is only set when this room represents an invite.
pub fn inviter(&self) -> Option<Member> {
self.imp().inviter.borrow().clone()
}
/// Load the member that invited us to this room, when applicable.
async fn load_inviter(&self) {
let Some(session) = self.session() else {
return;
};
let matrix_room = self.matrix_room();
if matrix_room.state() != RoomState::Invited {
return;
}
let own_user_id = self.session().user_id().to_owned();
let own_user_id = session.user_id().to_owned();
let matrix_room_clone = matrix_room.clone();
let handle =
spawn_tokio!(async move { matrix_room_clone.get_member_no_sync(&own_user_id).await });
@ -1020,7 +912,7 @@ impl Room {
inviter.update_from_room_member(&inviter_member);
self.imp().inviter.replace(Some(inviter));
self.notify("inviter");
self.notify_inviter();
}
/// Update the room state based on the new sync response
@ -1074,19 +966,12 @@ impl Room {
}
}
}
self.session()
.verification_list()
.handle_response_room(self.clone(), events);
}
/// The timestamp of the room's latest activity.
///
/// This is the timestamp of the latest event that counts as possibly
/// unread.
///
/// If it is not known, it will return `0`.
pub fn latest_activity(&self) -> u64 {
self.imp().latest_activity.get()
if let Some(session) = self.session() {
session
.verification_list()
.handle_response_room(self.clone(), events);
}
}
/// Set the timestamp of the room's latest possibly unread event.
@ -1096,7 +981,7 @@ impl Room {
}
self.imp().latest_activity.set(latest_activity);
self.notify("latest-activity");
self.notify_latest_activity();
}
fn load_power_levels(&self) {
@ -1209,7 +1094,7 @@ impl Room {
&self,
room_action: PowerLevelAction,
) -> gtk::ClosureExpression {
let session = self.session();
let session = self.session().unwrap();
let user_id = session.user_id().to_owned();
self.power_levels()
.member_is_allowed_to_expr(user_id, room_action)
@ -1324,12 +1209,6 @@ impl Room {
};
self.imp().predecessor_id.set(predecessor.room_id).unwrap();
self.notify("predecessor-id");
}
/// Whether this room was tombstoned.
pub fn is_tombstoned(&self) -> bool {
self.matrix_room().is_tombstoned()
}
/// The ID of the successor of this Room, if this room was upgraded.
@ -1337,16 +1216,10 @@ impl Room {
self.imp().successor_id.get().map(std::ops::Deref::deref)
}
/// The successor of this Room, if this room was upgraded and the successor
/// was joined.
pub fn successor(&self) -> Option<Room> {
self.imp().successor.upgrade()
}
/// Set the successor of this Room.
fn set_successor(&self, successor: &Room) {
self.imp().successor.set(Some(successor));
self.notify("successor")
self.notify_successor();
}
/// Load the tombstone for this room.
@ -1361,16 +1234,17 @@ impl Room {
imp.successor_id
.set(room_tombstone.replacement_room)
.unwrap();
self.notify("successor-id");
};
if !self.update_outdated() {
self.session()
.room_list()
.add_tombstoned_room(self.room_id().to_owned());
if let Some(session) = self.session() {
session
.room_list()
.add_tombstoned_room(self.room_id().to_owned());
}
}
self.notify("is-tombstoned");
self.notify_is_tombstoned();
}
/// Update whether this `Room` is outdated.
@ -1383,7 +1257,9 @@ impl Room {
return true;
}
let session = self.session();
let Some(session) = self.session() else {
return false;
};
let room_list = session.room_list();
if let Some(successor_id) = self.successor_id() {
@ -1507,17 +1383,6 @@ impl Room {
}
}
/// Set the most recent active verification for a user in this room.
pub fn set_verification(&self, verification: IdentityVerification) {
self.imp().verification.replace(Some(verification));
self.notify("verification");
}
/// The most recent active verification for a user in this room.
pub fn verification(&self) -> Option<IdentityVerification> {
self.imp().verification.borrow().clone()
}
/// Update the latest activity of the room with the given events.
///
/// The events must be in reverse chronological order.
@ -1534,14 +1399,9 @@ impl Room {
self.set_latest_activity(latest_activity);
}
/// Whether this room is encrypted.
pub fn is_encrypted(&self) -> bool {
self.imp().is_encrypted.get()
}
/// Set whether this room is encrypted.
pub fn set_is_encrypted(&self, is_encrypted: bool) {
let was_encrypted = self.is_encrypted();
let was_encrypted = self.encrypted();
if was_encrypted == is_encrypted {
return;
}
@ -1574,8 +1434,8 @@ impl Room {
return;
}
self.imp().is_encrypted.set(true);
self.notify("encrypted");
self.imp().encrypted.set(true);
self.notify_encrypted();
}
/// Get a `Pill` representing this `Room`.
@ -1614,39 +1474,40 @@ impl Room {
// Check if this is a 1-to-1 room to see if we can use a fallback.
// We don't have the active member count for invited rooms so process them too.
if avatar_url.is_none() && members_count > 0 && members_count <= 2 {
let handle =
spawn_tokio!(async move { matrix_room.members(RoomMemberships::ACTIVE).await });
let members = match handle.await.unwrap() {
Ok(m) => m,
Err(e) => {
error!("Failed to load room members: {e}");
vec![]
}
};
if let Some(session) = self.session() {
if avatar_url.is_none() && members_count > 0 && members_count <= 2 {
let handle =
spawn_tokio!(async move { matrix_room.members(RoomMemberships::ACTIVE).await });
let members = match handle.await.unwrap() {
Ok(m) => m,
Err(e) => {
error!("Failed to load room members: {e}");
vec![]
}
};
let session = self.session();
let own_user_id = session.user_id();
let mut has_own_member = false;
let mut other_member = None;
let own_user_id = session.user_id();
let mut has_own_member = false;
let mut other_member = None;
// Get the other member from the list.
for member in members {
if member.user_id() == own_user_id {
has_own_member = true;
} else {
other_member = Some(member);
// Get the other member from the list.
for member in members {
if member.user_id() == own_user_id {
has_own_member = true;
} else {
other_member = Some(member);
}
if has_own_member && other_member.is_some() {
break;
}
}
if has_own_member && other_member.is_some() {
break;
}
}
// Fallback to other user's avatar if this is a 1-to-1 room.
if members_count == 1 || (members_count == 2 && has_own_member) {
if let Some(other_member) = other_member {
avatar_url = other_member.avatar_url().map(ToOwned::to_owned)
// Fallback to other user's avatar if this is a 1-to-1 room.
if members_count == 1 || (members_count == 2 && has_own_member) {
if let Some(other_member) = other_member {
avatar_url = other_member.avatar_url().map(ToOwned::to_owned)
}
}
}
}
@ -1657,11 +1518,6 @@ impl Room {
.set_uri(avatar_url.map(String::from));
}
/// Whether anyone can join this room.
pub fn is_join_rule_public(&self) -> bool {
self.imp().is_join_rule_public.get()
}
/// Set whether anyone can join this room.
fn set_is_join_rule_public(&self, is_public: bool) {
if self.is_join_rule_public() == is_public {
@ -1669,6 +1525,6 @@ impl Room {
}
self.imp().is_join_rule_public.set(is_public);
self.notify("is-join-rule-public");
self.notify_is_join_rule_public();
}
}

View File

@ -22,13 +22,14 @@ pub const POWER_LEVEL_MIN: i64 = -POWER_LEVEL_MAX;
mod imp {
use std::cell::RefCell;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::PowerLevels)]
pub struct PowerLevels {
pub content: RefCell<BoxedPowerLevelsEventContent>,
/// The source of the power levels information.
#[property(get)]
pub power_levels: RefCell<BoxedPowerLevelsEventContent>,
}
#[glib::object_subclass]
@ -37,29 +38,12 @@ mod imp {
type Type = super::PowerLevels;
}
impl ObjectImpl for PowerLevels {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecBoxed::builder::<BoxedPowerLevelsEventContent>("power-levels")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"power-levels" => self.obj().power_levels().to_value(),
_ => unimplemented!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for PowerLevels {}
}
glib::wrapper! {
/// The power levels of a room.
pub struct PowerLevels(ObjectSubclass<imp::PowerLevels>);
}
@ -68,15 +52,10 @@ impl PowerLevels {
glib::Object::new()
}
/// The source of the power levels information.
pub fn power_levels(&self) -> BoxedPowerLevelsEventContent {
self.imp().content.borrow().clone()
}
/// Returns whether the member with the given user ID is allowed to do the
/// given action.
pub fn member_is_allowed_to(&self, user_id: &UserId, room_action: PowerLevelAction) -> bool {
let content = self.imp().content.borrow().0.clone();
let content = self.imp().power_levels.borrow().0.clone();
RoomPowerLevels::from(content).user_can_do(user_id, room_action)
}
@ -100,8 +79,8 @@ impl PowerLevels {
/// Updates the power levels from the given event.
pub fn update_from_event(&self, event: OriginalSyncStateEvent<RoomPowerLevelsEventContent>) {
let content = BoxedPowerLevelsEventContent(event.content);
self.imp().content.replace(content);
self.notify("power-levels");
self.imp().power_levels.replace(content);
self.notify_power_levels();
}
}

View File

@ -53,16 +53,19 @@ impl From<BackPaginationStatus> for TimelineState {
const MAX_BATCH_SIZE: u16 = 20;
mod imp {
use std::cell::{Cell, RefCell};
use glib::object::WeakRef;
use once_cell::{sync::Lazy, unsync::OnceCell};
use std::{
cell::{Cell, OnceCell, RefCell},
marker::PhantomData,
};
use super::*;
#[derive(Debug)]
#[derive(Debug, glib::Properties)]
#[properties(wrapper_type = super::Timeline)]
pub struct Timeline {
pub room: WeakRef<Room>,
/// The room containing this timeline.
#[property(get, set = Self::set_room, construct_only)]
pub room: glib::WeakRef<Room>,
/// The underlying SDK timeline.
pub timeline: OnceCell<Arc<SdkTimeline>>,
/// Items added at the start of the timeline.
@ -72,14 +75,20 @@ mod imp {
/// Items added at the end of the timeline.
pub end_items: gio::ListStore,
/// The `GListModel` containing all the timeline items.
#[property(get)]
pub items: gtk::FlattenListModel,
/// A Hashmap linking `EventKey` to corresponding `Event`
pub event_map: RefCell<HashMap<EventKey, Event>>,
/// The state of the timeline.
#[property(get, builder(TimelineState::default()))]
pub state: Cell<TimelineState>,
/// Whether this timeline has a typing row.
pub has_typing: Cell<bool>,
pub diff_handle: OnceCell<AbortHandle>,
pub back_pagination_status_handle: OnceCell<AbortHandle>,
/// Whether the timeline is empty.
#[property(get = Self::is_empty)]
pub empty: PhantomData<bool>,
}
impl Default for Timeline {
@ -105,6 +114,7 @@ mod imp {
has_typing: Default::default(),
diff_handle: Default::default(),
back_pagination_status_handle: Default::default(),
empty: Default::default(),
}
}
}
@ -115,45 +125,8 @@ mod imp {
type Type = super::Timeline;
}
#[glib::derived_properties]
impl ObjectImpl for Timeline {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<Room>("room")
.construct_only()
.build(),
glib::ParamSpecObject::builder::<gio::ListModel>("items")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("empty").read_only().build(),
glib::ParamSpecEnum::builder::<TimelineState>("state")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"room" => self.obj().set_room(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"room" => obj.room().to_value(),
"items" => obj.items().to_value(),
"empty" => obj.is_empty().to_value(),
"state" => obj.state().to_value(),
_ => unimplemented!(),
}
}
fn dispose(&self) {
if let Some(handle) = self.diff_handle.get() {
handle.abort();
@ -163,6 +136,33 @@ mod imp {
}
}
}
impl Timeline {
/// Set the room containing this timeline.
fn set_room(&self, room: Option<Room>) {
let obj = self.obj();
self.room.set(room.as_ref());
if let Some(room) = room {
room.typing_list().connect_items_changed(
clone!(@weak obj => move |list, _, _, _| {
if !list.is_empty() {
obj.add_typing_row();
}
}),
);
}
spawn!(clone!(@weak obj => async move {
obj.setup_timeline().await;
}));
}
/// Whether the timeline is empty.
fn is_empty(&self) -> bool {
self.sdk_items.n_items() == 0
}
}
}
glib::wrapper! {
@ -179,11 +179,6 @@ impl Timeline {
glib::Object::builder().property("room", room).build()
}
/// The `GListModel` containing the timeline items.
pub fn items(&self) -> &gio::ListModel {
self.imp().items.upcast_ref()
}
/// The `GListModel` containing only the items provided by the SDK.
pub fn sdk_items(&self) -> &gio::ListModel {
self.imp().sdk_items.upcast_ref()
@ -191,10 +186,12 @@ impl Timeline {
/// Update this `Timeline` with the given diff.
fn update(&self, diff: VectorDiff<Arc<SdkTimelineItem>>) {
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
let sdk_items = &imp.sdk_items;
let room = self.room();
let was_empty = self.is_empty();
let was_empty = self.empty();
match diff {
VectorDiff::Append { values } => {
@ -326,8 +323,8 @@ impl Timeline {
}
}
if self.is_empty() != was_empty {
self.notify("empty");
if self.empty() != was_empty {
self.notify_empty();
}
}
@ -368,7 +365,8 @@ impl Timeline {
/// Create a `TimelineItem` in this `Timeline` from the given SDK timeline
/// item.
fn create_item(&self, item: &SdkTimelineItem) -> TimelineItem {
let item = TimelineItem::new(item, &self.room());
let room = self.room().unwrap();
let item = TimelineItem::new(item, &room);
if let Some(event) = item.downcast_ref::<Event>() {
self.imp()
@ -378,7 +376,7 @@ impl Timeline {
// Keep track of the activity of the sender.
if event.counts_as_unread() {
if let Some(members) = self.room().members() {
if let Some(members) = room.members() {
let member = members.get_or_create(event.sender_id());
member.set_latest_activity(event.origin_server_ts_u64());
}
@ -482,7 +480,9 @@ impl Timeline {
if let Some(event) = self.event_by_key(&EventKey::EventId(event_id.clone())) {
event.raw().unwrap().deserialize().map_err(Into::into)
} else {
let room = self.room();
let Some(room) = self.room() else {
return Err(MatrixError::UnknownError("Failed to upgrade Room".into()));
};
let matrix_room = room.matrix_room();
let event_id_clone = event_id.clone();
let handle =
@ -498,29 +498,12 @@ impl Timeline {
}
}
/// Set the room containing this timeline.
fn set_room(&self, room: Option<Room>) {
self.imp().room.set(room.as_ref());
if let Some(room) = room {
room.typing_list().connect_items_changed(
clone!(@weak self as obj => move |list, _, _, _| {
if !list.is_empty() {
obj.add_typing_row();
}
}),
);
}
spawn!(clone!(@weak self as obj => async move {
obj.setup_timeline().await;
}));
}
/// Setup the underlying SDK timeline.
async fn setup_timeline(&self) {
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
let room = self.room();
let room_id = room.room_id().to_owned();
let matrix_room = room.matrix_room();
@ -614,7 +597,10 @@ impl Timeline {
/// Setup the back-pagination status.
async fn setup_back_pagination_status(&self) {
let room_id = self.room().room_id().to_owned();
let Some(room) = self.room() else {
return;
};
let room_id = room.room_id().to_owned();
let matrix_timeline = self.matrix_timeline();
let mut subscriber = matrix_timeline.back_pagination_status();
@ -646,11 +632,6 @@ impl Timeline {
.unwrap();
}
/// The room containing this timeline.
pub fn room(&self) -> Room {
self.imp().room.upgrade().unwrap()
}
/// The underlying SDK timeline.
pub fn matrix_timeline(&self) -> Arc<SdkTimeline> {
self.imp().timeline.get().unwrap().clone()
@ -677,17 +658,7 @@ impl Timeline {
_ => start_items.remove_all(),
}
self.notify("state");
}
/// The state of the timeline.
pub fn state(&self) -> TimelineState {
self.imp().state.get()
}
/// Whether the timeline is empty.
pub fn is_empty(&self) -> bool {
self.imp().sdk_items.n_items() == 0
self.notify_state();
}
fn has_typing_row(&self) -> bool {
@ -703,7 +674,7 @@ impl Timeline {
}
pub fn remove_empty_typing_row(&self) {
if !self.has_typing_row() || !self.room().typing_list().is_empty() {
if !self.has_typing_row() || !self.room().is_some_and(|r| r.typing_list().is_empty()) {
return;
}
@ -715,7 +686,13 @@ impl Timeline {
/// Returns `None` if it is not possible to know, for example if there are
/// no events in the Timeline.
pub async fn has_unread_messages(&self) -> Option<bool> {
let own_user_id = self.room().session().user_id().to_owned();
let Some(room) = self.room() else {
return None;
};
let Some(session) = room.session() else {
return None;
};
let own_user_id = session.user_id().to_owned();
let matrix_timeline = self.matrix_timeline();
let user_receipt_item = spawn_tokio!(async move {

View File

@ -6,9 +6,7 @@ use super::VirtualItem;
use crate::session::model::{Event, Room};
mod imp {
use std::cell::Cell;
use once_cell::sync::Lazy;
use std::{cell::Cell, marker::PhantomData};
use super::*;
@ -45,9 +43,34 @@ mod imp {
(klass.as_ref().event_sender_id)(this)
}
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::TimelineItem)]
pub struct TimelineItem {
/// A unique ID for this `TimelineItem`.
///
/// For debugging purposes.
#[property(get = Self::id)]
pub id: PhantomData<String>,
/// Whether this `TimelineItem` is selectable.
///
/// Defaults to `false`.
#[property(get = Self::selectable)]
pub selectable: PhantomData<bool>,
/// Whether this `TimelineItem` should show its header.
///
/// Defaults to `false`.
#[property(get, set = Self::set_show_header, explicit_notify)]
pub show_header: Cell<bool>,
/// Whether this `TimelineItem` is allowed to hide its header.
///
/// Defaults to `false`.
#[property(get = Self::can_hide_header)]
pub can_hide_header: PhantomData<bool>,
/// If this is a Matrix event, the sender of the event.
///
/// Defaults to `None`.
#[property(get = Self::event_sender_id)]
pub event_sender_id: PhantomData<Option<String>>,
}
#[glib::object_subclass]
@ -58,51 +81,46 @@ mod imp {
type Class = TimelineItemClass;
}
impl ObjectImpl for TimelineItem {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::builder("id").read_only().build(),
glib::ParamSpecBoolean::builder("selectable")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("show-header")
.explicit_notify()
.build(),
glib::ParamSpecBoolean::builder("can-hide-header")
.read_only()
.build(),
glib::ParamSpecString::builder("event-sender-id")
.read_only()
.build(),
]
});
#[glib::derived_properties]
impl ObjectImpl for TimelineItem {}
PROPERTIES.as_ref()
impl TimelineItem {
/// A unique ID for this `TimelineItem`.
///
/// For debugging purposes.
pub fn id(&self) -> String {
imp::timeline_item_id(&self.obj())
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"show-header" => self.obj().set_show_header(value.get().unwrap()),
_ => unimplemented!(),
}
/// Whether this `TimelineItem` is selectable.
///
/// Defaults to `false`.
pub fn selectable(&self) -> bool {
imp::timeline_item_selectable(&self.obj())
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"id" => obj.id().to_value(),
"selectable" => obj.selectable().to_value(),
"show-header" => obj.show_header().to_value(),
"can-hide-header" => obj.can_hide_header().to_value(),
"event-sender-id" => obj
.event_sender_id()
.as_ref()
.map(|u| u.as_str())
.to_value(),
_ => unimplemented!(),
/// Set whether this `TimelineItem` should show its header.
pub fn set_show_header(&self, show: bool) {
if self.show_header.get() == show {
return;
}
self.show_header.set(show);
self.obj().notify_show_header();
}
/// Whether this `TimelineItem` is allowed to hide its header.
///
/// Defaults to `false`.
pub fn can_hide_header(&self) -> bool {
imp::timeline_item_can_hide_header(&self.obj())
}
/// If this is a Matrix event, the sender of the event.
///
/// Defaults to `None`.
pub fn event_sender_id(&self) -> Option<String> {
imp::timeline_item_event_sender_id(&self.obj()).map(Into::into)
}
}
}
@ -180,30 +198,23 @@ pub trait TimelineItemExt: 'static {
impl<O: IsA<TimelineItem>> TimelineItemExt for O {
fn id(&self) -> String {
imp::timeline_item_id(self.upcast_ref())
self.upcast_ref().id()
}
fn selectable(&self) -> bool {
imp::timeline_item_selectable(self.upcast_ref())
self.upcast_ref().selectable()
}
fn show_header(&self) -> bool {
self.upcast_ref().imp().show_header.get()
self.upcast_ref().show_header()
}
fn set_show_header(&self, show: bool) {
let item = self.upcast_ref();
if item.show_header() == show {
return;
}
item.imp().show_header.set(show);
item.notify("show-header");
self.upcast_ref().set_show_header(show);
}
fn can_hide_header(&self) -> bool {
imp::timeline_item_can_hide_header(self.upcast_ref())
self.upcast_ref().can_hide_header()
}
fn event_sender_id(&self) -> Option<OwnedUserId> {

View File

@ -1,3 +1,5 @@
use std::ops::Deref;
use gtk::{glib, prelude::*, subclass::prelude::*};
use matrix_sdk_ui::timeline::VirtualTimelineItem;
use ruma::MilliSecondsSinceUnixEpoch;
@ -15,27 +17,35 @@ pub enum VirtualItemKind {
}
impl VirtualItemKind {
/// Convert this into a [`VirtualItemKindBoxed`].
fn boxed(self) -> VirtualItemKindBoxed {
VirtualItemKindBoxed(self)
/// Convert this into a [`BoxedVirtualItemKind`].
fn boxed(self) -> BoxedVirtualItemKind {
BoxedVirtualItemKind(self)
}
}
#[derive(Clone, Debug, PartialEq, Eq, glib::Boxed)]
#[boxed_type(name = "VirtualItemKindBoxed")]
struct VirtualItemKindBoxed(VirtualItemKind);
#[derive(Clone, Debug, Default, PartialEq, Eq, glib::Boxed)]
#[boxed_type(name = "BoxedVirtualItemKind")]
pub struct BoxedVirtualItemKind(VirtualItemKind);
impl Deref for BoxedVirtualItemKind {
type Target = VirtualItemKind;
fn deref(&self) -> &Self::Target {
&self.0
}
}
mod imp {
use std::cell::RefCell;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::VirtualItem)]
pub struct VirtualItem {
/// The kind of virtual item.
pub kind: RefCell<VirtualItemKind>,
#[property(get, set, construct)]
pub kind: RefCell<BoxedVirtualItemKind>,
}
#[glib::object_subclass]
@ -45,34 +55,12 @@ mod imp {
type ParentType = TimelineItem;
}
impl ObjectImpl for VirtualItem {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecBoxed::builder::<VirtualItemKindBoxed>("kind")
.construct()
.write_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"kind" => {
let boxed = value.get::<VirtualItemKindBoxed>().unwrap();
self.kind.replace(boxed.0);
}
_ => unimplemented!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for VirtualItem {}
impl TimelineItemImpl for VirtualItem {
fn id(&self) -> String {
match self.obj().kind() {
match &**self.kind.borrow() {
VirtualItemKind::Spinner => "VirtualItem::Spinner".to_owned(),
VirtualItemKind::Typing => "VirtualItem::Typing".to_owned(),
VirtualItemKind::TimelineStart => "VirtualItem::TimelineStart".to_owned(),
@ -145,9 +133,4 @@ impl VirtualItem {
.property("kind", VirtualItemKind::DayDivider(date).boxed())
.build()
}
/// The kind of virtual item.
pub fn kind(&self) -> VirtualItemKind {
self.imp().kind.borrow().clone()
}
}

View File

@ -5,28 +5,18 @@ use super::Member;
mod imp {
use std::cell::{Cell, RefCell};
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::TypingList)]
pub struct TypingList {
/// The list of members currently typing.
pub members: RefCell<Vec<Member>>,
/// Whether this list is empty.
#[property(get, set = Self::set_is_empty, explicit_notify)]
pub is_empty: Cell<bool>,
}
impl Default for TypingList {
fn default() -> Self {
Self {
members: Default::default(),
is_empty: Cell::new(true),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for TypingList {
const NAME: &'static str = "TypingList";
@ -34,25 +24,8 @@ mod imp {
type Interfaces = (gio::ListModel,);
}
impl ObjectImpl for TypingList {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecBoolean::builder("is-empty")
.default_value(true)
.read_only()
.build()]
});
PROPERTIES.as_ref()
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"is-empty" => self.obj().is_empty().to_value(),
_ => unimplemented!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for TypingList {}
impl ListModelImpl for TypingList {
fn item_type(&self) -> glib::Type {
@ -70,6 +43,18 @@ mod imp {
.map(|member| member.clone().upcast())
}
}
impl TypingList {
/// Set whether the list is empty.
fn set_is_empty(&self, is_empty: bool) {
if self.is_empty.get() == is_empty {
return;
}
self.is_empty.set(is_empty);
self.obj().notify_is_empty();
}
}
}
glib::wrapper! {
@ -87,24 +72,9 @@ impl TypingList {
self.imp().members.borrow().clone()
}
/// Set whether the list is empty.
fn set_is_empty(&self, empty: bool) {
self.imp().is_empty.set(empty);
self.notify("is-empty");
}
/// Whether the list is empty.
pub fn is_empty(&self) -> bool {
self.imp().is_empty.get()
}
pub fn update(&self, new_members: Vec<Member>) {
let prev_is_empty = self.is_empty();
if new_members.is_empty() {
if !prev_is_empty {
self.set_is_empty(true);
}
self.set_is_empty(true);
return;
}
@ -118,10 +88,7 @@ impl TypingList {
};
self.items_changed(0, removed, added);
if prev_is_empty {
self.set_is_empty(false);
}
self.set_is_empty(false);
}
}

View File

@ -161,7 +161,9 @@ impl Invite {
if category == RoomType::Left {
// We declined the invite or the invite was retracted, we should close the room
// if it is opened.
let session = room.session();
let Some(session) = room.session() else {
return;
};
let selection = session.sidebar_list_model().selection_model();
if let Some(selected_room) = selection.selected_item().and_downcast::<Room>() {
if selected_room == *room {

View File

@ -162,7 +162,7 @@ impl GeneralPage {
let expr_watch = AvatarData::this_expression("image")
.chain_property::<AvatarImage>("uri")
.watch(
Some(avatar_data),
Some(&avatar_data),
clone!(@weak self as obj, @weak avatar_data => move || {
obj.avatar_changed(avatar_data.image().and_then(|i| i.uri()));
}),
@ -170,24 +170,15 @@ impl GeneralPage {
imp.expr_watches.borrow_mut().push(expr_watch);
let room_handler_ids = vec![
room.connect_notify_local(
Some("name"),
clone!(@weak self as obj => move |room, _| {
obj.name_changed(room.name());
}),
),
room.connect_notify_local(
Some("topic"),
clone!(@weak self as obj => move |room, _| {
obj.topic_changed(room.topic());
}),
),
room.connect_notify_local(
Some("joined-members-count"),
clone!(@weak self as obj => move |room, _| {
obj.member_count_changed(room.joined_members_count());
}),
),
room.connect_name_notify(clone!(@weak self as obj => move |room| {
obj.name_changed(room.name());
})),
room.connect_topic_notify(clone!(@weak self as obj => move |room| {
obj.topic_changed(room.topic());
})),
room.connect_joined_members_count_notify(clone!(@weak self as obj => move |room| {
obj.member_count_changed(room.joined_members_count());
})),
];
self.member_count_changed(room.joined_members_count());
@ -288,7 +279,10 @@ impl GeneralPage {
mimetype: Some(info.mime.to_string()),
});
let client = room.session().client();
let Some(session) = room.session() else {
return;
};
let client = session.client();
let handle = spawn_tokio!(async move { client.media().upload(&info.mime, data).await });
let uri = match handle.await.unwrap() {

View File

@ -121,10 +121,11 @@ impl AudioRow {
imp.duration_label.set_label(&gettext("Unknown duration"));
}
let session = event.room().unwrap().session();
spawn!(clone!(@weak self as obj => async move {
obj.download_audio(audio, &session).await;
}));
if let Some(session) = event.room().and_then(|r| r.session()) {
spawn!(clone!(@weak self as obj, @weak session => async move {
obj.download_audio(audio, &session).await;
}));
}
}
}
}

View File

@ -72,7 +72,17 @@ impl HistoryViewerEvent {
pub async fn get_file_content(&self) -> Result<(String, Vec<u8>), matrix_sdk::Error> {
if let AnyMessageLikeEventContent::RoomMessage(content) = self.original_content().unwrap() {
let media = self.room().unwrap().session().client().media();
let Some(room) = self.room() else {
return Err(matrix_sdk::Error::UnknownError(
"Failed to upgrade Room".into(),
));
};
let Some(session) = room.session() else {
return Err(matrix_sdk::Error::UnknownError(
"Failed to upgrade Session".into(),
));
};
let media = session.client().media();
if let MessageType::File(content) = content.msgtype {
let filename = content

View File

@ -116,13 +116,19 @@ impl MediaItem {
}
if let Some(ref event) = event {
let Some(room) = event.room() else {
return;
};
let Some(session) = room.session() else {
return;
};
match event.original_content() {
Some(AnyMessageLikeEventContent::RoomMessage(message)) => match message.msgtype {
MessageType::Image(content) => {
self.show_image(content, &event.room().unwrap().session());
self.show_image(content, &session);
}
MessageType::Video(content) => {
self.show_video(content, &event.room().unwrap().session());
self.show_video(content, &session);
}
_ => {
panic!("Unexpected message type");

View File

@ -136,7 +136,7 @@ impl Timeline {
let room = self.room();
let matrix_room = room.matrix_room();
let last_token = imp.last_token.clone();
let is_encrypted = room.is_encrypted();
let is_encrypted = room.encrypted();
let handle: tokio::task::JoinHandle<matrix_sdk::Result<_>> = spawn_tokio!(async move {
let last_token = last_token.lock().await;

View File

@ -204,7 +204,9 @@ impl InviteeList {
search_term: String,
response: Result<search_users::v3::Response, HttpError>,
) {
let session = self.room().session();
let Some(session) = self.room().session() else {
return;
};
// We should have a strong reference to the list in the main page so we can use
// `get_or_create_members()`.
let member_list = self.room().get_or_create_members();
@ -312,7 +314,10 @@ impl InviteeList {
}
fn search_users(&self) {
let client = self.room().session().client();
let Some(session) = self.room().session() else {
return;
};
let client = session.client();
let search_term = if let Some(search_term) = self.search_term() {
search_term
} else {

View File

@ -275,7 +275,7 @@ impl ItemRow {
self.set_action_group(None);
self.set_event_actions(None);
match item.kind() {
match &*item.kind() {
VirtualItemKind::Spinner => {
if !self.child().map_or(false, |widget| widget.is::<Spinner>()) {
let spinner = Spinner::default();
@ -297,7 +297,8 @@ impl ItemRow {
self.room_history()
.room()
.as_ref()
.map(|room| room.typing_list()),
.map(|room| room.typing_list())
.as_ref(),
);
}
VirtualItemKind::TimelineStart => {
@ -427,12 +428,11 @@ impl ItemRow {
/// Unsets the actions if `event` is `None`.
fn set_event_actions(&self, event: Option<&Event>) -> Option<gio::SimpleActionGroup> {
self.clear_expression_watches();
let event = match event {
Some(event) => event,
None => {
self.insert_action_group("event", gio::ActionGroup::NONE);
return None;
}
let Some((event, room, session)) =
event.and_then(|e| e.room().and_then(|r| r.session().map(|s| (e, r, s))))
else {
self.insert_action_group("event", gio::ActionGroup::NONE);
return None;
};
let action_group = gio::SimpleActionGroup::new();
@ -454,7 +454,10 @@ impl ItemRow {
// Create a permalink
gio::ActionEntry::builder("permalink")
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
let matrix_room = event.room().matrix_room();
let Some(room) = event.room() else {
return;
};
let matrix_room = room.matrix_room();
let event_id = event.event_id().unwrap();
spawn!(clone!(@weak widget => async move {
let handle = spawn_tokio!(async move {
@ -477,7 +480,6 @@ impl ItemRow {
]);
if let TimelineItemContent::Message(message) = event.content() {
let session = event.room().session();
let own_user_id = session.user_id();
let is_from_own_user = event.sender_id() == own_user_id;
@ -503,8 +505,7 @@ impl ItemRow {
if is_from_own_user {
update_remove_action(self, &action_group, true);
} else {
let remove_watch = event
.room()
let remove_watch = room
.own_user_is_allowed_to_expr(PowerLevelAction::Redact)
.watch(
glib::Object::NONE,
@ -726,6 +727,9 @@ impl ItemRow {
let Some(event_id) = event.event_id() else {
return;
};
let Some(room) = event.room() else {
return;
};
let confirm_dialog = adw::MessageDialog::builder()
.transient_for(&window)
@ -745,7 +749,7 @@ impl ItemRow {
return;
}
if let Err(error) = event.room().redact(event_id, None).await {
if let Err(error) = room.redact(event_id, None).await {
error!("Failed to redact event: {error}");
toast!(self, gettext("Failed to remove message"));
}
@ -759,7 +763,9 @@ impl ItemRow {
let Some(event_id) = event.event_id() else {
return;
};
let room = event.room();
let Some(room) = event.room() else {
return;
};
let reaction_group = event.reactions().reaction_group_by_key(&key);
if let Some(reaction_key) = reaction_group.and_then(|group| group.user_reaction_event_key())

View File

@ -125,6 +125,10 @@ impl MessageContent {
}
pub fn update_for_event(&self, event: &Event) {
let Some(room) = event.room() else {
return;
};
let format = self.format();
if format == ContentFormat::Natural {
if let Some(related_content) = event.reply_to_event_content() {
@ -146,7 +150,6 @@ impl MessageContent {
);
}
TimelineDetails::Ready(related_content) => {
let room = event.room();
// We should have a strong reference to the list in the RoomHistory so we
// can use `get_or_create_members()`.
let sender = room
@ -177,7 +180,7 @@ impl MessageContent {
}
}
build_content(self, event.content(), format, event.sender(), &event.room());
build_content(self, event.content(), format, event.sender(), &room);
}
/// Get the texture displayed by this widget, if any.
@ -196,6 +199,10 @@ fn build_content(
sender: Member,
room: &Room,
) {
let Some(session) = room.session() else {
return;
};
let parent = parent.upcast_ref();
match content {
TimelineItemContent::Message(message) => {
@ -208,7 +215,7 @@ fn build_content(
parent.set_child(Some(&child));
child
};
child.audio(message.clone(), &room.session(), format);
child.audio(message.clone(), &session, format);
}
MessageType::Emote(message) => {
let child = if let Some(child) = parent.child().and_downcast::<MessageText>() {
@ -256,7 +263,7 @@ fn build_content(
parent.set_child(Some(&child));
child
};
child.image(message.clone(), &room.session(), format);
child.image(message.clone(), &session, format);
}
MessageType::Location(message) => {
let child =
@ -317,7 +324,7 @@ fn build_content(
parent.set_child(Some(&child));
child
};
child.video(message.clone(), &room.session(), format);
child.video(message.clone(), &session, format);
}
MessageType::VerificationRequest(_) => {
// TODO: show more information about the verification
@ -351,7 +358,7 @@ fn build_content(
parent.set_child(Some(&child));
child
};
child.sticker(sticker.content().clone(), &room.session(), format);
child.sticker(sticker.content().clone(), &session, format);
}
TimelineItemContent::UnableToDecrypt(_) => {
let child = if let Some(child) = parent.child().and_downcast::<MessageText>() {

View File

@ -195,6 +195,9 @@ impl MessageRow {
}
pub fn set_event(&self, event: Event) {
let Some(room) = event.room() else {
return;
};
let imp = self.imp();
// Remove signals and bindings from the previous event.
@ -243,8 +246,8 @@ impl MessageRow {
);
imp.reactions
.set_reaction_list(&event.room().get_or_create_members(), event.reactions());
imp.read_receipts.set_source(event.read_receipts());
.set_reaction_list(&room.get_or_create_members(), &event.reactions());
imp.read_receipts.set_source(&event.read_receipts());
imp.event
.set(event, vec![timestamp_handler, source_handler]);
self.notify("event");

View File

@ -135,9 +135,9 @@ impl MessageReaction {
fn set_group(&self, group: ReactionGroup) {
let imp = self.imp();
let key = group.key();
imp.reaction_key.set_label(key);
imp.reaction_key.set_label(&key);
if EMOJI_REGEX.is_match(key) {
if EMOJI_REGEX.is_match(&key) {
imp.reaction_key.add_css_class("emoji");
} else {
imp.reaction_key.remove_css_class("emoji");

View File

@ -431,6 +431,9 @@ impl MessageToolbar {
/// Set the event to edit.
pub fn set_edit(&self, event: Event) {
let Some(room) = event.room() else {
return;
};
// We don't support editing non-text messages.
let Some((text, formatted)) = event.message().and_then(|msg| match msg {
MessageType::Emote(emote) => Some((format!("/me {}", emote.body), emote.formatted)),
@ -443,7 +446,7 @@ impl MessageToolbar {
let mentions = if let Some(html) =
formatted.and_then(|f| (f.format == MessageFormat::Html).then_some(f.body))
{
let (_, mentions) = extract_mentions(&html, &event.room());
let (_, mentions) = extract_mentions(&html, &room);
let mut pos = 0;
// This is looking for the mention link's inner text in the Markdown
// so it is not super reliable: if there is other text that matches
@ -880,7 +883,10 @@ impl MessageToolbar {
fn update_completion(&self, room: Option<&Room>) {
let completion = &self.imp().completion;
completion.set_user_id(room.map(|r| r.session().user_id().to_string()));
completion.set_user_id(
room.and_then(|r| r.session())
.map(|s| s.user_id().to_string()),
);
// `RoomHistory` should have a strong reference to the list so we can use
// `get_or_create_members()`.
completion.set_members(room.map(|r| r.get_or_create_members()));
@ -926,8 +932,9 @@ impl MessageToolbar {
}
fn set_up_can_send_messages(&self, room: Option<&Room>) {
if let Some(room) = room {
let own_user_id = room.session().user_id().to_owned();
if let Some((room, own_user_id)) =
room.and_then(|r| r.session().map(|s| (r, s.user_id().to_owned())))
{
let imp = self.imp();
let own_member = room
@ -936,12 +943,9 @@ impl MessageToolbar {
// We don't need to keep the handler around, the member should be dropped when
// switching rooms.
own_member.connect_notify_local(
Some("membership"),
clone!(@weak self as obj => move |_, _| {
obj.update_can_send_messages();
}),
);
own_member.connect_membership_notify(clone!(@weak self as obj => move |_| {
obj.update_can_send_messages();
}));
imp.own_member.set(Some(&own_member));
let power_levels_handler = room.power_levels().connect_notify_local(

View File

@ -495,7 +495,7 @@ impl RoomHistory {
.replace(room.as_ref().map(|r| r.get_or_create_members()));
let model = room.as_ref().map(|room| room.timeline().items());
self.selection_model().set_model(model);
self.selection_model().set_model(model.as_ref());
imp.is_loading.set(false);
imp.room.replace(room);
@ -630,7 +630,7 @@ impl RoomHistory {
let imp = self.imp();
if let Some(room) = &*imp.room.borrow() {
if room.timeline().is_empty() {
if room.timeline().empty() {
if room.timeline().state() == TimelineState::Error {
imp.stack.set_visible_child(&*imp.error);
} else {
@ -654,7 +654,7 @@ impl RoomHistory {
return false;
}
if timeline.is_empty() {
if timeline.empty() {
// We definitely want messages if the timeline is ready but empty.
return true;
};
@ -784,7 +784,7 @@ impl RoomHistory {
};
let timeline = room.timeline();
if !timeline.is_empty() {
if !timeline.empty() {
let imp = self.imp();
if let Some(source_id) = imp.scroll_timeout.take() {
@ -934,6 +934,9 @@ impl RoomHistory {
let Some(room) = self.room() else {
return;
};
let Some(session) = room.session() else {
return;
};
if !room.is_joined() || !room.is_tombstoned() {
return;
@ -944,11 +947,10 @@ impl RoomHistory {
return;
};
let session = room.session();
window.show_room(session.session_id(), successor.room_id());
} else if let Some(successor_id) = room.successor_id().map(ToOwned::to_owned) {
spawn!(clone!(@weak self as obj, @weak room => async move {
if let Err(error) = room.session()
spawn!(clone!(@weak self as obj, @weak session => async move {
if let Err(error) = session
.room_list()
.join_by_id_or_alias(successor_id.into(), vec![]).await
{

View File

@ -121,12 +121,16 @@ impl StateRow {
}
let imp = self.imp();
imp.read_receipts.set_source(event.read_receipts());
imp.read_receipts.set_source(&event.read_receipts());
imp.event.replace(Some(event));
self.notify("event");
}
fn update_with_other_state(&self, event: &Event, other_state: &OtherState) {
let Some(room) = event.room() else {
return;
};
let widget = match other_state.content() {
AnyOtherFullStateEventContent::RoomCreate(content) => {
WidgetType::Creation(StateCreation::new(content))
@ -152,7 +156,7 @@ impl StateRow {
))
}
AnyOtherFullStateEventContent::RoomTombstone(_) => {
WidgetType::Tombstone(StateTombstone::new(&event.room()))
WidgetType::Tombstone(StateTombstone::new(&room))
}
_ => {
warn!(

View File

@ -129,7 +129,9 @@ impl StateTombstone {
let Some(room) = self.room() else {
return;
};
let session = room.session();
let Some(session) = room.session() else {
return;
};
let room_list = session.room_list();
// Join or view the room with the given identifier.

View File

@ -414,8 +414,11 @@ impl MediaViewer {
let Some(message) = self.message() else {
return;
};
let Some(session) = room.session() else {
return;
};
let client = room.session().client();
let client = session.client();
match &message {
MessageType::Image(image) => {
@ -540,7 +543,10 @@ impl MediaViewer {
let Some(message) = self.message() else {
return;
};
let client = room.session().client();
let Some(session) = room.session() else {
return;
};
let client = session.client();
let (filename, data) = match get_media_content(client, message).await {
Ok(res) => res,

View File

@ -331,6 +331,9 @@ impl SessionView {
/// Show a media event.
pub fn show_media(&self, event: &Event, source_widget: &impl IsA<gtk::Widget>) {
let Some(room) = event.room() else {
return;
};
let Some(message) = event.message() else {
error!("Trying to open the media viewer with an event that is not a message");
return;
@ -338,7 +341,7 @@ impl SessionView {
let imp = self.imp();
imp.media_viewer
.set_message(&event.room(), event.event_id().unwrap(), message);
.set_message(&room, event.event_id().unwrap(), message);
imp.media_viewer.reveal(source_widget);
}
}

View File

@ -289,7 +289,7 @@ pub async fn get_media_content(
/// Returns a new string with placeholders and the corresponding widgets and the
/// string they are replacing.
pub fn extract_mentions(s: &str, room: &Room) -> (String, Vec<(Pill, String)>) {
let session = room.session();
let session = room.session().unwrap();
let mut mentions = Vec::new();
let mut mention = None;
let mut new_string = String::new();