From d2ca0262aef815735646dfc63712d2b4fcc27bc3 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:46:21 +0200 Subject: [PATCH] feat(element-call url params): split url params into configuration and properties (#5560) This PR is part of an onging effort to move responsiblity to the EC app and out of the EX apps. 4 intends (f.ex `join_existing` `start_new_dm`... ) (as url paramters) are introduced in recent element call versions. Those intends behave like defaults. If an intend is set a set of url parameters are predefined. Not all params can be covered by the intend (for insteance the `widget_id` or the `host_url`). This PR splits the url parameters into configuration (things that can be configured by the intent) and properties (things that still need to be passed one by one) The goal with this change is that EX only needs to configre the intent once and the EC codebase can update the behavior in those 4 specific scenarios in case new features come along (auto hangup when other participants leave, send call ring notification...) Signed-off-by: Timo K - [ ] Public API changes documented in changelogs (optional) Signed-off-by: --------- Signed-off-by: Timo K --- bindings/matrix-sdk-ffi/src/widget.rs | 5 +- crates/matrix-sdk/CHANGELOG.md | 34 ++ crates/matrix-sdk/src/widget/mod.rs | 3 +- .../src/widget/settings/element_call.rs | 291 ++++++++++-------- crates/matrix-sdk/src/widget/settings/mod.rs | 4 +- 5 files changed, 210 insertions(+), 127 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/widget.rs b/bindings/matrix-sdk-ffi/src/widget.rs index cb5cd6547..9be1765b9 100644 --- a/bindings/matrix-sdk-ffi/src/widget.rs +++ b/bindings/matrix-sdk-ffi/src/widget.rs @@ -125,9 +125,10 @@ pub async fn generate_webview_url( /// call widget. #[matrix_sdk_ffi_macros::export] pub fn new_virtual_element_call_widget( - props: matrix_sdk::widget::VirtualElementCallWidgetOptions, + props: matrix_sdk::widget::VirtualElementCallWidgetProperties, + config: matrix_sdk::widget::VirtualElementCallWidgetConfig, ) -> Result { - Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props) + Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props, config) .map(|w| w.into())?) } diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 5cb4e268b..915ad5f79 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -84,6 +84,40 @@ All notable changes to this project will be documented in this file. ([#5431](https://github.com/matrix-org/matrix-rust-sdk/pull/5431)) - [**breaking**] `Room::send_call_notification` and `Room::send_call_notification_if_needed` have been removed, since the event type they send is outdated, and `Client` is not actually supposed to be able to join MatrixRTC sessions (yet). In practice, users of these methods probably already rely on another MatrixRTC implementation to participate in sessions, and such an implementation should be capable of sending notifications itself. ([#5452](https://github.com/matrix-org/matrix-rust-sdk/pull/5452)) +- [**breaking**] The `new_virtual_element_call_widget` now uses a `props` and a `config` parameter instead of only `props`. + This splits the configuration of the widget into required properties ("widget_id", "parent_url"...) so the widget can work + and optional config parameters ("skip_lobby", "header", "..."). + The config option should in most cases only provide the `"intent"` property. + All other config options will then be chosen by EC based on platform + `intent`. + + Before: + + ```rust + new_virtual_element_call_widget( + VirtualElementCallWidgetProperties { + widget_id: "my_widget_id", // required property + skip_lobby: Some(true), // optional configuration + preload: Some(true), // optional configuration + // ... + } + ) + ``` + + Now: + + ```rust + new_virtual_element_call_widget( + VirtualElementCallWidgetProperties { + widget_id: "my_widget_id", // required property + // ... only required properties + }, + VirtualElementCallWidgetConfig { + intend: Intend.StartCallDM, // defines the default values for all other configuration + skip_lobby: Some(false), // overwrite a specific default value + ..VirtualElementCallWidgetConfig::default() // set all other config options to `None`. Use defaults from intent. + } + ) + ``` ### Bugfix diff --git a/crates/matrix-sdk/src/widget/mod.rs b/crates/matrix-sdk/src/widget/mod.rs index 4e554b9af..95c00a330 100644 --- a/crates/matrix-sdk/src/widget/mod.rs +++ b/crates/matrix-sdk/src/widget/mod.rs @@ -45,7 +45,8 @@ pub use self::{ capabilities::{Capabilities, CapabilitiesProvider}, filter::{Filter, MessageLikeEventFilter, StateEventFilter, ToDeviceEventFilter}, settings::{ - ClientProperties, EncryptionSystem, Intent, VirtualElementCallWidgetOptions, WidgetSettings, + ClientProperties, EncryptionSystem, Intent, VirtualElementCallWidgetConfig, + VirtualElementCallWidgetProperties, WidgetSettings, }, }; diff --git a/crates/matrix-sdk/src/widget/settings/element_call.rs b/crates/matrix-sdk/src/widget/settings/element_call.rs index ff33241ef..4a0b1c5c5 100644 --- a/crates/matrix-sdk/src/widget/settings/element_call.rs +++ b/crates/matrix-sdk/src/widget/settings/element_call.rs @@ -26,9 +26,32 @@ use super::{url_params, WidgetSettings}; #[derive(Serialize)] #[serde(rename_all = "camelCase")] -/// Parameters for the Element Call widget. +/// Serialization struct for URL parameters for the Element Call widget. /// These are documented at https://github.com/element-hq/element-call/blob/livekit/docs/url-params.md -struct ElementCallParams { +/// +/// The ElementCallParams are used to be translated into url query parameters. +/// For all optional fields, the None case implies, that it will not be part of +/// the url parameters. +/// +/// # Example: +/// +/// ``` +/// ElementCallParams { +/// // Required parameters: +/// user_id: "@1234", +/// room_id: "$1234", +/// ... +/// // Optional configuration: +/// hide_screensharing: Some(true), +/// ..ElementCallParams::default() +/// } +/// ``` +/// will become: `my.url? ...requires_parameters... &hide_screensharing=true` +/// The reason it might be desirable to not list those configurations in the +/// URLs parameters is that the `intent` implies defaults for all configuration +/// values in the widget itself. Setting the URL parameter specifically will +/// overwrite those defaults. +struct ElementCallUrlParams { user_id: String, room_id: String, widget_id: String, @@ -43,14 +66,14 @@ struct ElementCallParams { /// Deprecated since Element Call v0.8.0. Included for backwards /// compatibility. Set to `true` if intent is `Intent::StartCall`. skip_lobby: Option, - confine_to_room: bool, - app_prompt: bool, + confine_to_room: Option, + app_prompt: Option, /// Supported since Element Call v0.13.0. - header: HeaderStyle, + header: Option, /// Deprecated since Element Call v0.13.0. Included for backwards /// compatibility. Use header: "standard"|"none" instead. hide_header: Option, - preload: bool, + preload: Option, /// Deprecated since Element Call v0.9.0. Included for backwards /// compatibility. Set to the same as `posthog_user_id`. analytics_id: Option, @@ -59,7 +82,7 @@ struct ElementCallParams { font_scale: Option, font: Option, #[serde(rename = "perParticipantE2EE")] - per_participant_e2ee: bool, + per_participant_e2ee: Option, password: Option, /// Supported since Element Call v0.8.0. intent: Option, @@ -73,8 +96,10 @@ struct ElementCallParams { sentry_dsn: Option, /// Supported since Element Call v0.9.0. Only used by the embedded package. sentry_environment: Option, - hide_screensharing: bool, - controlled_media_devices: bool, + /// Supported since Element Call v0.9.0. + hide_screensharing: Option, + /// Supported since Element Call v0.13.0. + controlled_audio_devices: Option, /// Supported since Element Call v0.14.0. send_notification_type: Option, } @@ -112,6 +137,11 @@ pub enum Intent { StartCall, /// The user wants to join an existing call. JoinExisting, + /// The user wants to join an existing call that is a "Direct Message" (DM) + /// room. + JoinExistingDM, + /// The user wants to start a call in a "Direct Message" (DM) room. + StartCallDM, } /// Defines how (if) element-call renders a header. @@ -130,19 +160,84 @@ pub enum HeaderStyle { /// Types of call notifications. #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] -#[derive(Debug, PartialEq, Serialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Clone, Default)] #[serde(rename_all = "snake_case")] pub enum NotificationType { - /// The receiving client should display a visual notification. + /// The receiving client should display a visual notification. + #[default] Notification, /// The receiving client should ring with an audible sound. Ring, } -/// Properties to create a new virtual Element Call widget. +/// Configuration parameters, to create a new virtual Element Call widget. +/// +/// If `intent` is provided the appropriate default values for all other +/// parameters will be used by element call. +/// In most cases its enough to only set the intent. Use the other properties +/// only if you want to deviate from the `intent` defaults. +/// +/// Set [`docs/url-params.md`](https://github.com/element-hq/element-call/blob/livekit/docs/url-params.md) +/// to find out more about the parameters and their defaults. #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[derive(Debug, Default, Clone)] -pub struct VirtualElementCallWidgetOptions { +pub struct VirtualElementCallWidgetConfig { + /// The intent of showing the call. + /// If the user wants to start a call or join an existing one. + /// Controls if the lobby is skipped or not. + pub intent: Option, + + /// Skip the lobby when joining a call. + pub skip_lobby: Option, + + /// Whether the branding header of Element call should be shown or if a + /// mobile header navbar should be render. + /// + /// Default: [`HeaderStyle::Standard`] + pub header: Option, + + /// Whether the branding header of Element call should be hidden. + /// + /// Default: `true` + #[deprecated(note = "Use `header` instead", since = "0.12.1")] + pub hide_header: Option, + + /// If set, the lobby will be skipped and the widget will join the + /// call on the `io.element.join` action. + /// + /// Default: `false` + pub preload: Option, + + /// Whether element call should prompt the user to open in the browser or + /// the app. + /// + /// Default: `false` + pub app_prompt: Option, + + /// Make it not possible to get to the calls list in the webview. + /// + /// Default: `true` + pub confine_to_room: Option, + + /// Do not show the screenshare button. + pub hide_screensharing: Option, + + /// Make the audio devices be controlled by the os instead of the + /// element-call webview. + pub controlled_audio_devices: Option, + + /// Whether and what type of notification Element Call should send, when + /// starting a call. + pub send_notification_type: Option, +} + +/// Properties to create a new virtual Element Call widget. +/// +/// All these are required to start the widget in the first place. +/// This is different from the `VirtualElementCallWidgetConfiguration` which +/// configures the widgets behavior. +#[derive(Debug, Default, uniffi::Record, Clone)] +pub struct VirtualElementCallWidgetProperties { /// The url to the app. /// /// E.g. , , @@ -166,40 +261,11 @@ pub struct VirtualElementCallWidgetOptions { /// usecase. pub parent_url: Option, - /// Whether the branding header of Element call should be shown or if a - /// mobile header navbar should be render. - /// - /// Default: [`HeaderStyle::Standard`] - pub header: Option, - - /// Whether the branding header of Element call should be hidden. - /// - /// Default: `true` - #[deprecated(note = "Use `header` instead", since = "0.12.1")] - pub hide_header: Option, - - /// If set, the lobby will be skipped and the widget will join the - /// call on the `io.element.join` action. - /// - /// Default: `false` - pub preload: Option, - /// The font scale which will be used inside element call. /// /// Default: `1` pub font_scale: Option, - /// Whether element call should prompt the user to open in the browser or - /// the app. - /// - /// Default: `false` - pub app_prompt: Option, - - /// Make it not possible to get to the calls list in the webview. - /// - /// Default: `true` - pub confine_to_room: Option, - /// The font to use, to adapt to the system font. pub font: Option, @@ -208,14 +274,6 @@ pub struct VirtualElementCallWidgetOptions { /// Use `EncryptionSystem::Unencrypted` to disable encryption. pub encryption: EncryptionSystem, - /// The intent of showing the call. - /// If the user wants to start a call or join an existing one. - /// Controls if the lobby is skipped or not. - pub intent: Option, - - /// Do not show the screenshare button. - pub hide_screensharing: bool, - /// Can be used to pass a PostHog id to element call. pub posthog_user_id: Option, /// The host of the posthog api. @@ -235,14 +293,6 @@ pub struct VirtualElementCallWidgetOptions { /// Sentry [environment](https://docs.sentry.io/concepts/key-terms/key-terms/) /// This is only used by the embedded package of Element Call. pub sentry_environment: Option, - //// - `true`: The webview should show the list of media devices it detects using - //// `enumerateDevices`. - /// - `false`: the webview shows a a list of devices injected by the - /// client. (used on ios & android) - pub controlled_media_devices: bool, - /// Whether and what type of notification Element Call should send, when - /// starting a call. - pub send_notification_type: Option, } impl WidgetSettings { @@ -260,17 +310,13 @@ impl WidgetSettings { /// * `props` - A struct containing the configuration parameters for a /// element call widget. pub fn new_virtual_element_call_widget( - props: VirtualElementCallWidgetOptions, + props: VirtualElementCallWidgetProperties, + config: VirtualElementCallWidgetConfig, ) -> Result { let mut raw_url: Url = Url::parse(&props.element_call_url)?; - let skip_lobby = if props.intent.as_ref().is_some_and(|x| x == &Intent::StartCall) { - Some(true) - } else { - None - }; #[allow(deprecated)] - let query_params = ElementCallParams { + let query_params = ElementCallUrlParams { user_id: url_params::USER_ID.to_owned(), room_id: url_params::ROOM_ID.to_owned(), widget_id: url_params::WIDGET_ID.to_owned(), @@ -282,20 +328,20 @@ impl WidgetSettings { base_url: url_params::HOMESERVER_URL.to_owned(), parent_url: props.parent_url.unwrap_or(props.element_call_url.clone()), - confine_to_room: props.confine_to_room.unwrap_or(true), - app_prompt: props.app_prompt.unwrap_or_default(), - header: props.header.unwrap_or_default(), - hide_header: props.hide_header, - preload: props.preload.unwrap_or_default(), + confine_to_room: config.confine_to_room, + app_prompt: config.app_prompt, + header: config.header, + hide_header: config.hide_header, + preload: config.preload, font_scale: props.font_scale, font: props.font, - per_participant_e2ee: props.encryption == EncryptionSystem::PerParticipantKeys, + per_participant_e2ee: Some(props.encryption == EncryptionSystem::PerParticipantKeys), password: match props.encryption { EncryptionSystem::SharedSecret { secret } => Some(secret), _ => None, }, - intent: props.intent, - skip_lobby, + intent: config.intent, + skip_lobby: config.skip_lobby, analytics_id: props.posthog_user_id.clone(), posthog_user_id: props.posthog_user_id, posthog_api_host: props.posthog_api_host, @@ -303,9 +349,9 @@ impl WidgetSettings { sentry_dsn: props.sentry_dsn, sentry_environment: props.sentry_environment, rageshake_submit_url: props.rageshake_submit_url, - hide_screensharing: props.hide_screensharing, - controlled_media_devices: props.controlled_media_devices, - send_notification_type: props.send_notification_type, + hide_screensharing: config.hide_screensharing, + controlled_audio_devices: config.controlled_audio_devices, + send_notification_type: config.send_notification_type, }; let query = @@ -331,46 +377,46 @@ mod tests { use ruma::api::client::profile::get_profile; use url::Url; - use crate::widget::{ClientProperties, Intent, WidgetSettings}; + use crate::widget::{ + settings::element_call::{HeaderStyle, VirtualElementCallWidgetConfig}, + ClientProperties, Intent, WidgetSettings, + }; const WIDGET_ID: &str = "1/@#w23"; - fn get_widget_settings( + fn get_element_call_widget_settings( encryption: Option, posthog: bool, rageshake: bool, sentry: bool, intent: Option, - controlle_output: bool, + controlled_output: bool, ) -> WidgetSettings { - let mut props = VirtualElementCallWidgetOptions { + let props = VirtualElementCallWidgetProperties { element_call_url: "https://call.element.io".to_owned(), widget_id: WIDGET_ID.to_owned(), + posthog_user_id: posthog.then(|| "POSTHOG_USER_ID".to_owned()), + posthog_api_host: posthog.then(|| "posthog.element.io".to_owned()), + posthog_api_key: posthog.then(|| "POSTHOG_KEY".to_owned()), + rageshake_submit_url: rageshake.then(|| "https://rageshake.element.io".to_owned()), + sentry_dsn: sentry.then(|| "SENTRY_DSN".to_owned()), + sentry_environment: sentry.then(|| "SENTRY_ENV".to_owned()), + encryption: encryption.unwrap_or(EncryptionSystem::PerParticipantKeys), + ..VirtualElementCallWidgetProperties::default() + }; + + let config = VirtualElementCallWidgetConfig { + controlled_audio_devices: Some(controlled_output), preload: Some(true), app_prompt: Some(true), confine_to_room: Some(true), - encryption: encryption.unwrap_or(EncryptionSystem::PerParticipantKeys), + hide_screensharing: Some(false), + header: Some(HeaderStyle::Standard), intent, - controlled_media_devices: controlle_output, - ..VirtualElementCallWidgetOptions::default() + ..VirtualElementCallWidgetConfig::default() }; - if posthog { - props.posthog_user_id = Some("POSTHOG_USER_ID".to_owned()); - props.posthog_api_host = Some("posthog.element.io".to_owned()); - props.posthog_api_key = Some("POSTHOG_KEY".to_owned()); - } - - if rageshake { - props.rageshake_submit_url = Some("https://rageshake.element.io".to_owned()); - } - - if sentry { - props.sentry_dsn = Some("SENTRY_DSN".to_owned()); - props.sentry_environment = Some("SENTRY_ENV".to_owned()); - } - - WidgetSettings::new_virtual_element_call_widget(props) + WidgetSettings::new_virtual_element_call_widget(props, config) .expect("could not parse virtual element call widget") } @@ -390,7 +436,7 @@ mod tests { use serde_html_form::from_str; - use super::{EncryptionSystem, VirtualElementCallWidgetOptions}; + use super::{EncryptionSystem, VirtualElementCallWidgetProperties}; fn get_query_sets(url: &Url) -> Option<(QuerySet, QuerySet)> { let fq = from_str::(url.fragment_query().unwrap_or_default()).ok()?; @@ -400,7 +446,8 @@ mod tests { #[test] fn test_new_virtual_element_call_widget_base_url() { - let widget_settings = get_widget_settings(None, false, false, false, None, false); + let widget_settings = + get_element_call_widget_settings(None, false, false, false, None, false); assert_eq!(widget_settings.base_url().unwrap().as_str(), "https://call.element.io/"); } @@ -424,10 +471,12 @@ mod tests { &preload=true\ &perParticipantE2EE=true\ &hideScreensharing=false\ - &controlledMediaDevices=false\ + &controlledAudioDevices=false\ "; - let mut url = get_widget_settings(None, false, false, false, None, false).raw_url().clone(); + let mut url = get_element_call_widget_settings(None, false, false, false, None, false) + .raw_url() + .clone(); let mut gen = Url::parse(CONVERTED_URL).unwrap(); assert_eq!(get_query_sets(&url).unwrap(), get_query_sets(&gen).unwrap()); url.set_fragment(None); @@ -440,7 +489,7 @@ mod tests { #[test] fn test_new_virtual_element_call_widget_id() { assert_eq!( - get_widget_settings(None, false, false, false, None, false).widget_id(), + get_element_call_widget_settings(None, false, false, false, None, false).widget_id(), WIDGET_ID ); } @@ -485,9 +534,9 @@ mod tests { &clientId=io.my_matrix.client\ &perParticipantE2EE=true\ &hideScreensharing=false\ - &controlledMediaDevices=false\ + &controlledAudioDevices=false\ "; - let gen = build_url_from_widget_settings(get_widget_settings( + let gen = build_url_from_widget_settings(get_element_call_widget_settings( None, false, false, false, None, false, )); @@ -526,9 +575,9 @@ mod tests { &rageshakeSubmitUrl=https%3A%2F%2Frageshake.element.io\ &sentryDsn=SENTRY_DSN\ &sentryEnvironment=SENTRY_ENV\ - &controlledMediaDevices=false\ + &controlledAudioDevices=false\ "; - let gen = build_url_from_widget_settings(get_widget_settings( + let gen = build_url_from_widget_settings(get_element_call_widget_settings( None, true, true, true, None, false, )); @@ -546,7 +595,7 @@ mod tests { fn test_password_url_props_from_widget_settings() { { // PerParticipantKeys - let url = build_url_from_widget_settings(get_widget_settings( + let url = build_url_from_widget_settings(get_element_call_widget_settings( Some(EncryptionSystem::PerParticipantKeys), false, false, @@ -565,7 +614,7 @@ mod tests { } { // Unencrypted - let url = build_url_from_widget_settings(get_widget_settings( + let url = build_url_from_widget_settings(get_element_call_widget_settings( Some(EncryptionSystem::Unencrypted), false, false, @@ -582,7 +631,7 @@ mod tests { } { // SharedSecret - let url = build_url_from_widget_settings(get_widget_settings( + let url = build_url_from_widget_settings(get_element_call_widget_settings( Some(EncryptionSystem::SharedSecret { secret: "this_surely_is_save".to_owned() }), false, false, @@ -605,7 +654,7 @@ mod tests { fn test_controlled_output_url_props_from_widget_settings() { { // PerParticipantKeys - let url = build_url_from_widget_settings(get_widget_settings( + let url = build_url_from_widget_settings(get_element_call_widget_settings( Some(EncryptionSystem::PerParticipantKeys), false, false, @@ -613,11 +662,11 @@ mod tests { None, true, )); - let controlled_media_element = ("controlledMediaDevices".to_owned(), "true".to_owned()); + let controlled_audio_element = ("controlledAudioDevices".to_owned(), "true".to_owned()); let query_set = get_query_sets(&Url::parse(&url).unwrap()).unwrap().1; assert!( - query_set.contains(&controlled_media_element), - "The query elements: \n{query_set:?}\nDid not contain: \n{controlled_media_element:?}" + query_set.contains(&controlled_audio_element), + "The query elements: \n{query_set:?}\nDid not contain: \n{controlled_audio_element:?}" ); } } @@ -626,7 +675,7 @@ mod tests { fn test_intent_url_props_from_widget_settings() { { // no intent - let url = build_url_from_widget_settings(get_widget_settings( + let url = build_url_from_widget_settings(get_element_call_widget_settings( None, false, false, false, None, false, )); let query_set = get_query_sets(&Url::parse(&url).unwrap()).unwrap().1; @@ -642,7 +691,7 @@ mod tests { } { // Intent::JoinExisting - let url = build_url_from_widget_settings(get_widget_settings( + let url = build_url_from_widget_settings(get_element_call_widget_settings( None, false, false, @@ -668,7 +717,7 @@ mod tests { } { // Intent::StartCall - let url = build_url_from_widget_settings(get_widget_settings( + let url = build_url_from_widget_settings(get_element_call_widget_settings( None, false, false, @@ -678,11 +727,7 @@ mod tests { )); let query_set = get_query_sets(&Url::parse(&url).unwrap()).unwrap().1; - // skipLobby should be set for compatibility with versions < 0.8.0 - let expected_elements = [ - ("intent".to_owned(), "start_call".to_owned()), - ("skipLobby".to_owned(), "true".to_owned()), - ]; + let expected_elements = [("intent".to_owned(), "start_call".to_owned())]; for e in expected_elements { assert!( query_set.contains(&e), diff --git a/crates/matrix-sdk/src/widget/settings/mod.rs b/crates/matrix-sdk/src/widget/settings/mod.rs index 7bbd80ad5..5496b41ee 100644 --- a/crates/matrix-sdk/src/widget/settings/mod.rs +++ b/crates/matrix-sdk/src/widget/settings/mod.rs @@ -24,7 +24,9 @@ use crate::Room; mod element_call; mod url_params; -pub use self::element_call::{EncryptionSystem, Intent, VirtualElementCallWidgetOptions}; +pub use self::element_call::{ + EncryptionSystem, Intent, VirtualElementCallWidgetConfig, VirtualElementCallWidgetProperties, +}; /// Settings of the widget. #[derive(Debug, Clone)]