diff --git a/po/POTFILES.in b/po/POTFILES.in index 61acb991..41219a83 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -108,7 +108,7 @@ src/session/view/sidebar/room_row.rs src/session/view/sidebar/row.rs src/shortcuts.ui src/user_facing_error.rs -src/utils/matrix.rs +src/utils/matrix/mod.rs src/utils/media.rs src/utils/message_dialog.rs src/window.rs diff --git a/src/session/model/room/mod.rs b/src/session/model/room/mod.rs index 39912ae1..b49a6e7f 100644 --- a/src/session/model/room/mod.rs +++ b/src/session/model/room/mod.rs @@ -21,6 +21,7 @@ use matrix_sdk::{ DisplayName, HttpError, Result as MatrixResult, RoomInfo, RoomMemberships, RoomState, }; use ruma::{ + api::client::config::set_room_account_data, events::{ reaction::ReactionEventContent, receipt::{ReceiptEventContent, ReceiptType}, @@ -34,6 +35,7 @@ use ruma::{ AnyMessageLikeEventContent, AnyRoomAccountDataEvent, AnySyncStateEvent, AnySyncTimelineEvent, SyncEphemeralRoomEvent, SyncStateEvent, }, + serde::Raw, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, }; use tracing::{debug, error, warn}; @@ -53,7 +55,13 @@ use super::{ room_list::RoomMetainfo, AvatarData, AvatarImage, AvatarUriSource, IdentityVerification, Session, SidebarItem, SidebarItemImpl, User, }; -use crate::{components::Pill, gettext_f, prelude::*, spawn, spawn_tokio}; +use crate::{ + components::Pill, + gettext_f, + prelude::*, + spawn, spawn_tokio, + utils::matrix::custom_events::{LanguageEvent, LanguageEventContent}, +}; mod imp { use std::cell::Cell; @@ -105,6 +113,8 @@ mod imp { pub typing_list: TypingList, /// Whether anyone can join this room. pub is_join_rule_public: Cell, + /// The language to spell check in this room. + pub language: RefCell>, } #[glib::object_subclass] @@ -183,6 +193,9 @@ mod imp { glib::ParamSpecBoolean::builder("is-join-rule-public") .explicit_notify() .build(), + glib::ParamSpecString::builder("language") + .read_only() + .build(), ] }); @@ -231,6 +244,7 @@ mod imp { "encrypted" => obj.is_encrypted().to_value(), "typing-list" => obj.typing_list().to_value(), "is-join-rule-public" => obj.is_join_rule_public().to_value(), + "language" => obj.language().to_value(), _ => unimplemented!(), } } @@ -360,6 +374,10 @@ impl Room { self.setup_receipts(); self.setup_typing(); + spawn!(clone!(@weak self as obj => async move { + obj.load_language().await; + })); + spawn!(clone!(@weak self as obj => async move { obj.watch_room_info().await; })); @@ -1314,21 +1332,28 @@ impl Room { } pub fn handle_left_response(&self, response_room: LeftRoom) { + self.update_for_room_account_data(response_room.account_data); self.update_for_events(response_room.timeline.events); } pub fn handle_joined_response(&self, response_room: JoinedRoom) { - if response_room - .account_data - .iter() - .any(|e| matches!(e.deserialize(), Ok(AnyRoomAccountDataEvent::Tag(_)))) - { - self.load_category(); - } - + self.update_for_room_account_data(response_room.account_data); self.update_for_events(response_room.timeline.events); } + fn update_for_room_account_data(&self, room_account_data: Vec>) { + for raw in room_account_data { + if let Ok(AnyRoomAccountDataEvent::Tag(_)) = raw.deserialize() { + self.load_category(); + continue; + } + + if let Ok(language) = raw.deserialize_as::() { + self.set_language_inner(Some(language.content.input_language)); + } + } + } + /// Connect to the signal sent when a room was forgotten. pub fn connect_room_forgotten(&self, f: F) -> glib::SignalHandlerId { self.connect_local("room-forgotten", true, move |values| { @@ -1701,4 +1726,85 @@ impl Room { self.imp().is_join_rule_public.set(is_public); self.notify("is-join-rule-public"); } + + async fn load_language(&self) { + let matrix_room = self.matrix_room(); + + let handle = spawn_tokio!(async move { + matrix_room + .account_data_static::() + .await + }); + + let raw = match handle.await.unwrap() { + Ok(Some(raw)) => raw, + Ok(None) => return, + Err(error) => { + error!(room_id = %self.room_id(), "Failed to load language room account data: {error}"); + return; + } + }; + + match raw.deserialize() { + Ok(ev) => { + self.set_language_inner(Some(ev.content.input_language)); + } + Err(error) => { + error!(room_id = %self.room_id(), "Failed to deserialize language room account data: {error}"); + } + } + } + + /// The language to spell check in this room. + pub fn language(&self) -> Option { + self.imp().language.borrow().clone() + } + + /// Set the language to spell check in this room. + pub fn set_language(&self, language: Option) { + if !self.set_language_inner(language.clone()) { + return; + } + + if let Some(language) = language { + spawn!(clone!(@weak self as obj => async move { + obj.set_room_account_data_language(language).await; + })); + } + } + + fn set_language_inner(&self, language: Option) -> bool { + let imp = self.imp(); + if imp.language.borrow().as_ref() == language.as_ref() { + return false; + } + + imp.language.replace(language.clone()); + self.notify("language"); + true + } + + async fn set_room_account_data_language(&self, input_language: String) { + let client = self.session().client(); + let user_id = client.user_id().unwrap().to_owned(); + let room_id = self.room_id().to_owned(); + + let request = match set_room_account_data::v3::Request::new( + user_id, + room_id, + &LanguageEventContent { input_language }, + ) { + Ok(req) => req, + Err(error) => { + error!(room_id = %self.room_id(), "Failed to build request for language room account data: {error}"); + return; + } + }; + + let handle = spawn_tokio!(async move { client.send(request, None,).await }); + + if let Err(error) = handle.await.unwrap() { + error!(room_id = %self.room_id(), "Failed to send language room account data: {error}"); + } + } } diff --git a/src/utils/matrix/custom_events.rs b/src/utils/matrix/custom_events.rs new file mode 100644 index 00000000..a35dce3d --- /dev/null +++ b/src/utils/matrix/custom_events.rs @@ -0,0 +1,14 @@ +use ruma::events::macros::EventContent; +use serde::{Deserialize, Serialize}; + +/// The content of an `org.gnome.fractal.language` event. +/// +/// The language used in a room. +/// +/// It is used to change the spell checker's language per room. +#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] +#[ruma_event(type = "org.gnome.fractal.language", kind = RoomAccountData)] +pub struct LanguageEventContent { + /// The language to spell check. + pub input_language: String, +} diff --git a/src/utils/matrix.rs b/src/utils/matrix/mod.rs similarity index 99% rename from src/utils/matrix.rs rename to src/utils/matrix/mod.rs index 91d69334..369f359c 100644 --- a/src/utils/matrix.rs +++ b/src/utils/matrix/mod.rs @@ -13,6 +13,8 @@ use ruma::{ }; use thiserror::Error; +pub mod custom_events; + use super::media::filename_for_mime; use crate::{ components::{Pill, DEFAULT_PLACEHOLDER},