diff --git a/bindings/matrix-sdk-ffi/src/widget.rs b/bindings/matrix-sdk-ffi/src/widget.rs index 32b7d4e16d5..a14d9f4d6d3 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 5cb4e268bbe..915ad5f79c6 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 4e554b9afcb..95c00a3308b 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 ff33241efa2..4a0b1c5c5c1 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,41 +160,35 @@ 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 { - /// The url to the app. - /// - /// E.g. , , - pub element_call_url: String, +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, - /// The widget id. - pub widget_id: String, - - /// The url that is used as the target for the PostMessages sent - /// by the widget (to the client). - /// - /// For a web app client this is the client url. In case of using other - /// platforms the client most likely is setup up to listen to - /// postmessages in the same webview the widget is hosted. In this case - /// the `parent_url` is set to the url of the webview with the widget. Be - /// aware that this means that the widget will receive its own postmessage - /// messages. The `matrix-widget-api` (js) ignores those so this works but - /// it might break custom implementations. - /// - /// Defaults to `element_call_url` for the non-iframe (dedicated webview) - /// usecase. - pub parent_url: 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. @@ -184,11 +208,6 @@ pub struct VirtualElementCallWidgetOptions { /// 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. /// @@ -200,6 +219,53 @@ pub struct VirtualElementCallWidgetOptions { /// 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. , , + pub element_call_url: String, + + /// The widget id. + pub widget_id: String, + + /// The url that is used as the target for the PostMessages sent + /// by the widget (to the client). + /// + /// For a web app client this is the client url. In case of using other + /// platforms the client most likely is setup up to listen to + /// postmessages in the same webview the widget is hosted. In this case + /// the `parent_url` is set to the url of the webview with the widget. Be + /// aware that this means that the widget will receive its own postmessage + /// messages. The `matrix-widget-api` (js) ignores those so this works but + /// it might break custom implementations. + /// + /// Defaults to `element_call_url` for the non-iframe (dedicated webview) + /// usecase. + pub parent_url: Option, + + /// The font scale which will be used inside element call. + /// + /// Default: `1` + pub font_scale: 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 7bbd80ad5b2..5496b41ee82 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)]