Skip to main content

matrix_ui_serializable/init/
singletons.rs

1use anyhow::anyhow;
2use std::{
3    collections::HashMap,
4    path::PathBuf,
5    sync::{Arc, Condvar, LazyLock, Mutex, OnceLock},
6    time::Duration,
7};
8
9use matrix_sdk::{
10    Client, RoomMemberships,
11    ruma::{OwnedRoomId, OwnedUserId},
12};
13use matrix_sdk_ui::sync_service::SyncService;
14use tokio::{
15    sync::{
16        broadcast,
17        mpsc::{Receiver, UnboundedSender},
18    },
19    time::{Instant, interval},
20};
21
22use crate::{
23    events::timeline::TimelineKind,
24    init::session::ClientSession,
25    models::{
26        async_requests::MatrixRequest, event_bridge::EventBridge,
27        events::MatrixVerificationResponse,
28    },
29    submit_async_request,
30};
31
32/// The sender used by [`submit_async_request`] to send requests to the async worker thread.
33/// Currently there is only one, but it can be cloned if we need more concurrent senders.
34pub static REQUEST_SENDER: OnceLock<UnboundedSender<MatrixRequest>> = OnceLock::new();
35
36/// The singleton sync service.
37pub static SYNC_SERVICE: OnceLock<SyncService> = OnceLock::new();
38
39/// Flag set by `handle_rooms_loading_state` when all rooms are loaded.
40/// if rooms have been synced or not.
41pub static ALL_ROOMS_LOADED: OnceLock<bool> = OnceLock::new();
42
43pub(crate) struct TempClientSlot {
44    pub(crate) lock: Mutex<Option<Client>>,
45    pub(crate) cvar: Condvar,
46}
47
48/// The temporary Client used during login. It can be mutated
49/// because the user can change its homeserver.
50pub static TEMP_CLIENT: LazyLock<TempClientSlot> = LazyLock::new(|| TempClientSlot {
51    lock: Mutex::new(None),
52    cvar: Condvar::new(),
53});
54
55/// The temporary Client Session used during login
56pub static TEMP_CLIENT_SESSION: LazyLock<Mutex<Option<ClientSession>>> =
57    LazyLock::new(|| Mutex::new(None));
58
59/// The logged-in Matrix client, which can be freely and cheaply cloned.
60pub static CLIENT: OnceLock<Client> = OnceLock::new();
61
62/// The current User's ID
63pub static CURRENT_USER_ID: OnceLock<OwnedUserId> = OnceLock::new();
64
65/// Flag to be set once the frontend Login Store is up and ready
66pub static LOGIN_STORE_READY: OnceLock<bool> = OnceLock::new();
67
68pub static HAS_SESSION_STORED: OnceLock<bool> = OnceLock::new();
69
70#[derive(Debug, Clone)]
71pub enum UIUpdateMessage {
72    RefreshUI,
73}
74
75// Global broadcaster instance
76static GLOBAL_BROADCASTER: OnceLock<GlobalBroadcaster> = OnceLock::new();
77
78pub struct GlobalBroadcaster {
79    sender: broadcast::Sender<UIUpdateMessage>,
80}
81
82pub static APP_DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
83
84impl GlobalBroadcaster {
85    fn new(capacity: usize) -> Self {
86        let (sender, _) = broadcast::channel(capacity);
87        Self { sender }
88    }
89
90    fn broadcast(
91        &self,
92        message: UIUpdateMessage,
93    ) -> Result<usize, broadcast::error::SendError<UIUpdateMessage>> {
94        self.sender.send(message)
95    }
96
97    fn subscribe(&self) -> broadcast::Receiver<UIUpdateMessage> {
98        self.sender.subscribe()
99    }
100}
101
102// Initialize the global broadcaster (call this once at startup)
103pub fn init_broadcaster(capacity: usize) -> Result<(), &'static str> {
104    GLOBAL_BROADCASTER
105        .set(GlobalBroadcaster::new(capacity))
106        .map_err(|_| "Broadcaster already initialized")
107}
108
109// Globally available function to broadcast messages
110pub fn broadcast_event(message: UIUpdateMessage) {
111    let broadcaster = GLOBAL_BROADCASTER
112        .get()
113        .expect("Broadcaster not initialized. Call init_broadcaster() first.");
114
115    let _ = broadcaster.broadcast(message);
116}
117
118// Globally available function to create receivers
119pub fn subscribe_to_events() -> Result<broadcast::Receiver<UIUpdateMessage>, &'static str> {
120    let broadcaster = GLOBAL_BROADCASTER
121        .get()
122        .ok_or("Broadcaster not initialized. Call init_broadcaster() first.")?;
123
124    Ok(broadcaster.subscribe())
125}
126
127// Lib -> adapter communication
128
129pub static EVENT_BRIDGE: OnceLock<EventBridge> = OnceLock::new();
130
131pub fn get_event_bridge<'a>() -> anyhow::Result<&'a EventBridge> {
132    let bridge = EVENT_BRIDGE
133        .get()
134        .ok_or(anyhow!("The event bridge is not yet set"))?;
135    Ok(bridge)
136}
137
138// Adapter -> lib communication
139
140pub static VERIFICATION_RESPONSE_RECEIVER: OnceLock<
141    tokio::sync::Mutex<Receiver<MatrixVerificationResponse>>,
142> = OnceLock::new();
143
144pub async fn get_verification_response_receiver_lock<'a>()
145-> anyhow::Result<tokio::sync::MutexGuard<'a, Receiver<MatrixVerificationResponse>>> {
146    let recv = VERIFICATION_RESPONSE_RECEIVER
147        .get()
148        .ok_or(anyhow!("The verification response receiver is not yet set"))?;
149    Ok(recv.lock().await)
150}
151
152// Membership changes expiry Map.
153// Each time we receive a membership change from the Matrix sync
154// we add a RoomId in this map. After a debounced period of time,
155// this will send a request to update the members list.
156pub(crate) struct ExpiryMap {
157    // Stores the ID and the time it should expire
158    items: Arc<Mutex<HashMap<OwnedRoomId, Instant>>>,
159}
160
161impl ExpiryMap {
162    fn new() -> Self {
163        let items: Arc<Mutex<HashMap<OwnedRoomId, Instant>>> = Arc::new(Mutex::new(HashMap::new()));
164        let items_clone = Arc::clone(&items);
165
166        tokio::spawn(async move {
167            let mut ticker = interval(Duration::from_millis(500));
168
169            loop {
170                ticker.tick().await;
171
172                let mut expired_ids = Vec::new();
173                let now = Instant::now();
174
175                {
176                    let mut map = items_clone.lock().unwrap();
177
178                    // .retain() is the most efficient way to remove items.
179                    // If the closure returns 'false', the item is removed.
180                    map.retain(|id, &mut expiry| {
181                        if now >= expiry {
182                            expired_ids.push(id.to_owned());
183                            false
184                        } else {
185                            true
186                        }
187                    });
188                }
189
190                for id in expired_ids {
191                    submit_async_request(MatrixRequest::GetRoomMembers {
192                        timeline_kind: TimelineKind::MainRoom { room_id: id },
193                        memberships: RoomMemberships::JOIN,
194                        // Important so we don't try to fetch too many
195                        // members from too large rooms.
196                        local_only: true,
197                    });
198                }
199            }
200        });
201
202        ExpiryMap { items }
203    }
204
205    pub(crate) fn insert(&self, id: OwnedRoomId, duration: Duration) {
206        let expiry = Instant::now() + duration;
207        self.items.lock().unwrap().insert(id, expiry);
208    }
209}
210
211pub(crate) static MEMBERSHIP_UPDATES_EXPIRY_MAP: LazyLock<ExpiryMap> =
212    LazyLock::new(ExpiryMap::new);