matrix_sdk/widget/settings/mod.rs
1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use language_tags::LanguageTag;
16use ruma::{
17 DeviceId, RoomId, UserId,
18 api::client::profile::{AvatarUrl, DisplayName, get_profile},
19};
20use url::Url;
21
22use crate::Room;
23
24mod element_call;
25mod url_params;
26
27pub use self::element_call::{
28 EncryptionSystem, Intent, VirtualElementCallWidgetConfig, VirtualElementCallWidgetProperties,
29};
30
31/// Settings of the widget.
32#[derive(Debug, Clone)]
33pub struct WidgetSettings {
34 widget_id: String,
35 init_on_content_load: bool,
36 raw_url: Url,
37}
38
39impl WidgetSettings {
40 /// Create a new WidgetSettings instance
41 pub fn new(
42 id: String,
43 init_on_content_load: bool,
44 raw_url: &str,
45 ) -> Result<Self, url::ParseError> {
46 Ok(Self { widget_id: id, init_on_content_load, raw_url: Url::parse(raw_url)? })
47 }
48
49 /// Widget's unique identifier.
50 pub fn widget_id(&self) -> &str {
51 &self.widget_id
52 }
53
54 /// Whether or not the widget should be initialized on load message
55 /// (`ContentLoad` message), or upon creation/attaching of the widget to
56 /// the SDK's state machine that drives the API.
57 pub fn init_on_content_load(&self) -> bool {
58 self.init_on_content_load
59 }
60
61 /// This contains the url from the widget state event.
62 /// In this url placeholders can be used to pass information from the client
63 /// to the widget. Possible values are: `$matrix_widget_id`,
64 /// `$matrix_display_name`, etc.
65 ///
66 /// # Examples
67 ///
68 /// `http://widget.domain?username=$userId` will become
69 /// `http://widget.domain?username=@user_matrix_id:server.domain`.
70 pub fn raw_url(&self) -> &Url {
71 &self.raw_url
72 }
73
74 /// Get the base url of the widget. Used as the target for PostMessages. In
75 /// case the widget is in a webview and not an IFrame. It contains the
76 /// schema and the authority e.g. `https://my.domain.org`. A postmessage would
77 /// be sent using: `postMessage(myMessage, widget_base_url)`.
78 pub fn base_url(&self) -> Option<Url> {
79 base_url(&self.raw_url)
80 }
81
82 /// Create the actual [`Url`] that can be used to setup the WebView or
83 /// IFrame that contains the widget.
84 ///
85 /// # Arguments
86 ///
87 /// * `room` - A Matrix room which is used to query the logged in username
88 /// * `props` - Properties from the client that can be used by a widget to
89 /// adapt to the client. e.g. language, font-scale...
90 //
91 // TODO: add `From<WidgetStateEvent>`, so that `WidgetSettings` can be built
92 // by using the room state.
93 pub async fn generate_webview_url(
94 &self,
95 room: &Room,
96 props: ClientProperties,
97 ) -> Result<Url, url::ParseError> {
98 self._generate_webview_url(
99 room.client().account().fetch_user_profile().await.unwrap_or_default(),
100 room.own_user_id(),
101 room.room_id(),
102 room.client().device_id().unwrap_or("UNKNOWN".into()),
103 room.client().homeserver(),
104 props,
105 )
106 }
107
108 // Using a separate function (without Room as a param) for tests.
109 fn _generate_webview_url(
110 &self,
111 profile: get_profile::v3::Response,
112 user_id: &UserId,
113 room_id: &RoomId,
114 device_id: &DeviceId,
115 homeserver_url: Url,
116 client_props: ClientProperties,
117 ) -> Result<Url, url::ParseError> {
118 let avatar_url = profile
119 .get_static::<AvatarUrl>()
120 .ok()
121 .flatten()
122 .map(|url| url.to_string())
123 .unwrap_or_default();
124
125 let query_props = url_params::QueryProperties {
126 widget_id: self.widget_id.clone(),
127 avatar_url,
128 display_name: profile.get_static::<DisplayName>().ok().flatten().unwrap_or_default(),
129 user_id: user_id.into(),
130 room_id: room_id.into(),
131 language: client_props.language.to_string(),
132 client_theme: client_props.theme,
133 client_id: client_props.client_id,
134 device_id: device_id.into(),
135 homeserver_url: homeserver_url.into(),
136 };
137 let mut generated_url = self.raw_url.clone();
138 url_params::replace_properties(&mut generated_url, query_props);
139
140 Ok(generated_url)
141 }
142}
143
144/// The set of settings and properties for the widget based on the client
145/// configuration. Those values are used generate the widget url.
146#[derive(Debug)]
147pub struct ClientProperties {
148 /// The client_id provides the widget with the option to behave differently
149 /// for different clients. e.g org.example.ios.
150 client_id: String,
151 /// The language the client is set to e.g. en-us.
152 language: LanguageTag,
153 /// A string describing the theme (dark, light) or org.example.dark.
154 theme: String,
155}
156
157impl ClientProperties {
158 /// Creates client properties. If a malformatted language tag is provided,
159 /// the default one (en-US) will be used.
160 ///
161 /// # Arguments
162 /// * `client_id` - client identifier. This allows widgets to adapt to
163 /// specific clients (e.g. `io.element.web`).
164 /// * `language` - language that is used in the client (default: `en-US`).
165 /// * `theme` - theme (dark, light) or org.example.dark (default: `light`).
166 pub fn new(client_id: &str, language: Option<LanguageTag>, theme: Option<String>) -> Self {
167 // It is safe to unwrap "en-us".
168 let default_language = LanguageTag::parse("en-us").unwrap();
169 let default_theme = "light".to_owned();
170 Self {
171 language: language.unwrap_or(default_language),
172 client_id: client_id.to_owned(),
173 theme: theme.unwrap_or(default_theme),
174 }
175 }
176}
177
178fn base_url(url: &Url) -> Option<Url> {
179 let mut url = url.clone();
180 url.path_segments_mut().ok()?.clear();
181 url.set_query(None);
182 url.set_fragment(None);
183 Some(url)
184}