Skip to main content

matrix_sdk/
automatic_call_status.rs

1// Copyright 2026 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
15//! Automatic mirroring of this device's MatrixRTC participation into the
16//! [MSC4426] `m.call` profile field.
17//!
18//! [MSC4426]: https://github.com/matrix-org/matrix-spec-proposals/pull/4426
19
20use std::{
21    collections::HashSet,
22    sync::{Arc, Mutex},
23};
24
25use matrix_sdk_common::executor::spawn;
26use ruma::{
27    OwnedRoomId, SecondsSinceUnixEpoch,
28    events::{OriginalSyncStateEvent, call::member::CallMemberEventContent},
29};
30use tracing::warn;
31
32use crate::{Client, Room, client::WeakClient, event_handler::EventHandlerHandle};
33
34/// Owns the `m.call.member` event handler for auto-syncing the `m.call` profile
35/// field. Dropping this struct deregisters the handler. Holds a [`WeakClient`]
36/// rather than a strong `Client` to avoid a reference cycle.
37pub(crate) struct AutomaticCallStatus {
38    handle: EventHandlerHandle,
39    client: WeakClient,
40}
41
42/// Rooms in which this device is currently participating in an active MatrixRTC
43/// call. Maintained incrementally from `m.call.member` events.
44type ActiveCallRooms = Arc<Mutex<HashSet<OwnedRoomId>>>;
45
46impl Client {
47    /// Enable or disable automatic mirroring of this device's MatrixRTC
48    /// participation into the [MSC4426] `m.call` profile field. Off by default.
49    ///
50    /// Toggling `false -> true` registers a typed event handler for
51    /// `m.call.member` state events. Toggling `true -> false` deregisters it.
52    ///
53    /// Toggling `true -> false` does NOT clear `m.call` on the server, you
54    /// should call [`crate::Account::clear_call`] explicitly if that is
55    /// desired.
56    ///
57    /// [MSC4426]: https://github.com/matrix-org/matrix-spec-proposals/pull/4426
58    pub fn enable_automatic_call_status(&self, enabled: bool) {
59        let mut automatic_call_status = self.inner.automatic_call_status.lock().unwrap();
60        match (enabled, automatic_call_status.is_some()) {
61            (true, false) => {
62                *automatic_call_status = Some(AutomaticCallStatus::new(self));
63            }
64            (false, true) => *automatic_call_status = None,
65            _ => {}
66        }
67    }
68}
69
70impl AutomaticCallStatus {
71    fn new(client: &Client) -> Self {
72        // Start empty: `m.call` is shared across the user's devices, so we
73        // deliberately don't reconcile from current room state on start-up to
74        // avoid stomping on a status set by another device. The trade-off is
75        // that a crash/kill while on a call leaves `m.call` set until the user
76        // clears it manually.
77        let rooms: ActiveCallRooms = Arc::new(Mutex::new(HashSet::new()));
78        let handle = client.add_event_handler(
79            async move |event: OriginalSyncStateEvent<CallMemberEventContent>,
80                        room: Room,
81                        client: Client| {
82                on_event(&rooms, event, room, client);
83            },
84        );
85        let weak_client = client.downgrade();
86        Self { handle, client: weak_client }
87    }
88}
89
90impl Drop for AutomaticCallStatus {
91    fn drop(&mut self) {
92        if let Some(client) = self.client.get() {
93            client.remove_event_handler(self.handle.clone());
94        }
95    }
96}
97
98fn on_event(
99    rooms: &ActiveCallRooms,
100    event: OriginalSyncStateEvent<CallMemberEventContent>,
101    room: Room,
102    client: Client,
103) {
104    let Some(own_user_id) = client.user_id() else { return };
105    let Some(own_device_id) = client.device_id() else { return };
106
107    // Ignore events for other users' memberships.
108    if event.state_key.user_id() != own_user_id {
109        return;
110    }
111
112    // Update the aggregate for this room, then return early if the "in any
113    // call" boolean didn't flip.
114    let is_device_in_room_call = room.is_device_in_active_room_call(own_user_id, own_device_id);
115    let room_id = room.room_id().to_owned();
116    let (was_in_call, now_in_call) = {
117        let mut rooms = rooms.lock().unwrap();
118        let was_in_call = !rooms.is_empty();
119        if is_device_in_room_call {
120            rooms.insert(room_id);
121        } else {
122            rooms.remove(&room_id);
123        }
124        (was_in_call, !rooms.is_empty())
125    };
126    // Return early if no change.
127    if was_in_call == now_in_call {
128        return;
129    }
130    let active_call_rooms = rooms.clone();
131    spawn(async move {
132        let in_call = !active_call_rooms.lock().unwrap().is_empty();
133        let result = if in_call {
134            let joined_ts = Some(SecondsSinceUnixEpoch::now());
135            client.account().set_call(joined_ts).await
136        } else {
137            client.account().clear_call().await
138        };
139        if let Err(error) = result {
140            warn!(?error, in_call, "m.call auto-sync request failed");
141        }
142    });
143}