Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ rand = { version = "0.10.1", default-features = false, features = ["std", "std_r
regex = { version = "1.12.2", default-features = false }
reqwest = { version = "0.13.1", default-features = false }
rmp-serde = { version = "1.3.0", default-features = false }
ruma = { git = "https://github.com/ruma/ruma", rev = "8beb557b466a43b057c9c155277c77fe8766fadf", features = [
ruma = { git = "https://github.com/ruma/ruma", rev = "a494c58c541c63042b26380a9238a24bdda4d85c", features = [
"client-api-c",
"compat-unset-avatar",
"compat-upload-signatures",
Expand Down
5 changes: 4 additions & 1 deletion bindings/matrix-sdk-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ crate-type = [
]

[features]
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis", "experimental-push-secrets", "experimental-search"]
default = ["bundled-sqlite", "unstable-msc4274", "unstable-msc4426", "experimental-element-recent-emojis", "experimental-push-secrets", "experimental-search"]
experimental-search = ["matrix-sdk/experimental-search"]
# Use SQLite for the session storage.
sqlite = ["matrix-sdk/sqlite"]
Expand All @@ -33,6 +33,7 @@ bundled-sqlite = ["sqlite", "matrix-sdk/bundled-sqlite"]
# Use IndexedDB for the session storage.
indexeddb = ["matrix-sdk/indexeddb"]
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
unstable-msc4426 = ["matrix-sdk/unstable-msc4426"]
# Required when targeting a Javascript environment, like Wasm in a browser.
js = ["matrix-sdk-ui/js"]
# Enable sentry error monitoring, not compatible with Wasm platforms.
Expand Down Expand Up @@ -88,6 +89,8 @@ ruma = { workspace = true, features = [
"unstable-msc2545",
# Room language event type
"unstable-msc4334",
# User status profile fields (m.status, m.call).
"unstable-msc4426",
"unstable-uniffi",
] }
serde.workspace = true
Expand Down
1 change: 1 addition & 0 deletions bindings/matrix-sdk-ffi/changelog.d/6616.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Expose [MSC4426] user-status profile fields through the FFI.
60 changes: 59 additions & 1 deletion bindings/matrix-sdk-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ use matrix_sdk_ui::{
};
use mime::Mime;
use oauth2::Scope;
#[cfg(feature = "unstable-msc4426")]
use ruma::api::client::profile::{Call, Status};
use ruma::{
OwnedDeviceId, OwnedMxcUri, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
api::{
Expand Down Expand Up @@ -125,6 +127,8 @@ use super::{
room::{Room, room_info::RoomInfo},
session_verification::SessionVerificationController,
};
#[cfg(feature = "unstable-msc4426")]
use crate::ruma::{UserCall, UserStatus};
use crate::{
ClientError,
authentication::{
Expand Down Expand Up @@ -2494,6 +2498,17 @@ pub struct UserProfile {
pub user_id: String,
pub display_name: Option<String>,
pub avatar_url: Option<String>,

/// The user's status (MSC4426 `m.status` profile field), if set.
#[cfg(feature = "unstable-msc4426")]
pub status: Option<UserStatus>,

/// Set when the user is in a call (MSC4426 `m.call` profile field).
///
/// `None` means the user is not in a call. `Some(UserCall { call_joined_ts:
/// None })` means the user is in a call but the join time wasn't recorded.
#[cfg(feature = "unstable-msc4426")]
pub call: Option<UserCall>,
}

impl UserProfile {
Expand All @@ -2504,7 +2519,20 @@ impl UserProfile {
let display_name = response.get_static::<DisplayName>()?;
let avatar_url = response.get_static::<AvatarUrl>()?.map(|url| url.to_string());

Ok(UserProfile { user_id: user_id.to_string(), display_name, avatar_url })
#[cfg(feature = "unstable-msc4426")]
let status = response.get_static::<Status>()?.map(UserStatus::from);
#[cfg(feature = "unstable-msc4426")]
let call = response.get_static::<Call>()?.map(UserCall::from);

Ok(UserProfile {
user_id: user_id.to_string(),
display_name,
avatar_url,
#[cfg(feature = "unstable-msc4426")]
status,
#[cfg(feature = "unstable-msc4426")]
call,
})
}
}

Expand All @@ -2514,10 +2542,40 @@ impl From<&search_users::v3::User> for UserProfile {
user_id: value.user_id.to_string(),
display_name: value.display_name.clone(),
avatar_url: value.avatar_url.as_ref().map(|url| url.to_string()),
#[cfg(feature = "unstable-msc4426")]
status: None,
#[cfg(feature = "unstable-msc4426")]
call: None,
}
}
}

#[cfg(feature = "unstable-msc4426")]
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Set the current user's status (MSC4426 `m.status` profile field).
///
/// Replaces any existing status. Use [`Self::clear_user_status`] to
/// remove it.
pub async fn set_user_status(&self, status: UserStatus) -> Result<(), ClientError> {
self.inner.account().set_status(status.emoji, status.text).await?;
Ok(())
}

/// Clear the current user's status (MSC4426).
///
/// Deletes both `m.status` and `m.call` concurrently. Clearing `m.status`
/// alone would let `m.call` immediately reappear if the user were in a
/// call.
pub async fn clear_user_status(&self) -> Result<(), ClientError> {
let account = self.inner.account();
let (status_res, call_res) = tokio::join!(account.clear_status(), account.clear_call());
status_res?;
call_res?;
Ok(())
}
}

impl Client {
fn process_session_change(&self, session_change: SessionChange) {
if let Some(delegate_data) = self.delegate_data.get() {
Expand Down
55 changes: 55 additions & 0 deletions bindings/matrix-sdk-ffi/src/ruma.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ use ruma::{
},
serde::JsonObject,
};
#[cfg(feature = "unstable-msc4426")]
use ruma::{
SecondsSinceUnixEpoch,
profile::{CallProfileField, StatusProfileField},
};
use tracing::info;

use crate::{
Expand Down Expand Up @@ -220,6 +225,56 @@ impl From<RumaPresenceState> for PresenceState {
}
}

/// A user-set status (MSC4426 `m.status` profile field value).
#[cfg(feature = "unstable-msc4426")]
#[derive(Debug, Clone, uniffi::Record)]
pub struct UserStatus {
pub emoji: String,
pub text: String,
}

#[cfg(feature = "unstable-msc4426")]
impl From<UserStatus> for StatusProfileField {
fn from(value: UserStatus) -> Self {
Self::new(value.text, value.emoji)
}
}

#[cfg(feature = "unstable-msc4426")]
impl From<StatusProfileField> for UserStatus {
fn from(value: StatusProfileField) -> Self {
Self { emoji: value.emoji, text: value.text }
}
}

/// The user's call indicator (MSC4426 `m.call` profile field value).
///
/// Presence of a `UserCall` value means the user is in a call. The optional
/// `call_joined_ts` is the Unix-epoch seconds when they joined, if known.
#[cfg(feature = "unstable-msc4426")]
#[derive(Debug, Clone, uniffi::Record)]
pub struct UserCall {
pub call_joined_ts: Option<u64>,
}

#[cfg(feature = "unstable-msc4426")]
impl From<CallProfileField> for UserCall {
fn from(value: CallProfileField) -> Self {
Self { call_joined_ts: value.call_joined_ts.map(|ts| u64::from(ts.get())) }
}
}

#[cfg(feature = "unstable-msc4426")]
impl From<UserCall> for CallProfileField {
fn from(value: UserCall) -> Self {
let mut field = CallProfileField::new();
field.call_joined_ts = value
.call_joined_ts
.map(|secs| SecondsSinceUnixEpoch(UInt::try_from(secs).unwrap_or_default()));
field
}
}

#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_new(
msgtype: MessageType,
Expand Down
3 changes: 3 additions & 0 deletions crates/matrix-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ docsrs = ["e2e-encryption", "sqlite", "indexeddb", "sso-login", "qrcode", "feder
# Add support for inline media galleries via msgtypes
unstable-msc4274 = ["ruma/unstable-msc4274", "matrix-sdk-base/unstable-msc4274"]

# Add support for user status profile fields (m.status, m.call).
unstable-msc4426 = ["ruma/unstable-msc4426"]

experimental-search = ["matrix-sdk-search"]

experimental-element-recent-emojis = ["matrix-sdk-base/experimental-element-recent-emojis"]
Expand Down
3 changes: 3 additions & 0 deletions crates/matrix-sdk/changelog.d/6616.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added `Account::set_status`, `Account::clear_status`, `Account::set_call`, and
`Account::clear_call` to write [MSC4426] user-status profile fields (`m.status`
and `m.call`). All four are gated behind the new `unstable-msc4426` feature.
46 changes: 46 additions & 0 deletions crates/matrix-sdk/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ use matrix_sdk_base::{
store::StateStoreExt,
};
use mime::Mime;
#[cfg(feature = "unstable-msc4426")]
use ruma::SecondsSinceUnixEpoch;
#[cfg(feature = "experimental-element-recent-emojis")]
use ruma::api::client::config::set_global_account_data::v3::Request as UpdateGlobalAccountDataRequest;
#[cfg(feature = "unstable-msc4426")]
use ruma::profile::{CallProfileField, StatusProfileField};
use ruma::{
ClientSecret, MxcUri, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, SessionId, UInt, UserId,
api::{
Expand Down Expand Up @@ -446,6 +450,48 @@ impl Account {
Ok(response.value)
}

/// Set the user's status (MSC4426 `m.status` profile field).
///
/// Replaces any existing status. Use [`Self::clear_status`] to remove it.
///
/// # Arguments
///
/// * `emoji` - the status emoji. The MSC limits this to 32 bytes; not
/// enforced client-side.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why “not enforced client-side”? It would save one roundtrip to the server.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conversation on Ruma for reference ruma/ruma#2498 (comment). (Not saying that answers what we should do in the SDK, just noting that that's why Ruma doesn't do any checks on this)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. However, since the Rust SDK tries to be strict and consistent regarding the specification, I prefer to have a check here.

@ganfra ganfra Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of error should I return then? Can you point me to any parameter validation in the code base, because I fail to see one...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Hywan this is blocking us, do you think we can move without it?

/// * `text` - the status text. The MSC limits this to 256 bytes; not
/// enforced client-side.
Comment on lines +461 to +462

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question: why “not enforced client-side”? Same reason.

#[cfg(feature = "unstable-msc4426")]
pub async fn set_status(&self, emoji: String, text: String) -> Result<()> {
let value = StatusProfileField::new(text, emoji);
self.set_profile_field(ProfileFieldValue::Status(value)).await
}

/// Clear the user's status (deletes the MSC4426 `m.status` profile field).
#[cfg(feature = "unstable-msc4426")]
pub async fn clear_status(&self) -> Result<()> {
self.delete_profile_field(ProfileFieldName::Status).await
}

/// Set the user's call indicator (MSC4426 `m.call` profile field).
///
/// # Arguments
///
/// * `call_joined_ts` - when the user joined the current call, in seconds
/// since the Unix epoch. `None` if the joined time isn't known.
#[cfg(feature = "unstable-msc4426")]
pub async fn set_call(&self, call_joined_ts: Option<SecondsSinceUnixEpoch>) -> Result<()> {
let mut value = CallProfileField::new();
value.call_joined_ts = call_joined_ts;
self.set_profile_field(ProfileFieldValue::Call(value)).await
}

/// Clear the user's call indicator (deletes the MSC4426 `m.call` profile
/// field).
#[cfg(feature = "unstable-msc4426")]
pub async fn clear_call(&self) -> Result<()> {
self.delete_profile_field(ProfileFieldName::Call).await
}

/// Set the given field of our own user's profile.
///
/// [`Client::homeserver_capabilities()`] should be called first to check it
Expand Down
6 changes: 3 additions & 3 deletions crates/matrix-sdk/src/widget/machine/driver_req.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ impl MatrixDriverRequest for SendToDeviceRequest {
/// and `action`. Defined by [MSC4157](https://github.com/matrix-org/matrix-spec-proposals/pull/4157)
#[derive(Deserialize, Debug, Clone)]
pub(crate) struct UpdateDelayedEventRequest {
pub(crate) action: update_delayed_event::unstable::UpdateAction,
pub(crate) action: update_delayed_event::UpdateAction,
pub(crate) delay_id: String,
}

Expand All @@ -328,10 +328,10 @@ impl From<UpdateDelayedEventRequest> for MatrixDriverRequestData {
}

impl MatrixDriverRequest for UpdateDelayedEventRequest {
type Response = update_delayed_event::unstable::Response;
type Response = update_delayed_event::unstable_v1::Response;
}

impl FromMatrixDriverResponse for update_delayed_event::unstable::Response {
impl FromMatrixDriverResponse for update_delayed_event::unstable_v1::Response {
fn from_response(ev: MatrixDriverResponse) -> Option<Self> {
match ev {
MatrixDriverResponse::DelayedEventUpdated(response) => Some(response),
Expand Down
Loading
Loading