atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git
bindings/matrix-sdk-ffi/src/spaces.rsRust555 lines20.3 KB
1
// Copyright 2025 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
use std::{fmt::Debug, sync::Arc};
16
17
use eyeball_im::VectorDiff;
18
use futures_util::{StreamExt, pin_mut};
19
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
20
use matrix_sdk_ui::spaces::{
21
    SpaceFilter as UISpaceFilter, SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList,
22
    SpaceService as UISpaceService,
23
    leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
24
    room_list::SpaceRoomListPaginationState,
25
};
26
use ruma::RoomId;
27
28
use crate::{
29
    TaskHandle,
30
    client::JoinRule,
31
    error::ClientError,
32
    room::{Membership, RoomHero},
33
    room_preview::RoomType,
34
    runtime::get_runtime_handle,
35
};
36
37
/// The main entry point into the Spaces facilities.
38
///
39
/// The spaces service is responsible for retrieving one's joined rooms,
40
/// building a graph out of their `m.space.parent` and `m.space.child` state
41
/// events, and providing access to the top-level spaces and their children.
42
#[derive(uniffi::Object)]
43
pub struct SpaceService {
44
    inner: UISpaceService,
45
}
46
47
impl SpaceService {
48
    /// Creates a new `SpaceService` instance.
49
    pub(crate) fn new(inner: UISpaceService) -> Self {
50
        Self { inner }
51
    }
52
}
53
54
#[matrix_sdk_ffi_macros::export]
55
impl SpaceService {
56
    /// Returns a list of all the top-level joined spaces. It will eagerly
57
    /// compute the latest version and also notify subscribers if there were
58
    /// any changes.
59
    pub async fn top_level_joined_spaces(&self) -> Vec<SpaceRoom> {
60
        self.inner.top_level_joined_spaces().await.into_iter().map(Into::into).collect()
61
    }
62
63
    /// Subscribes to updates on the joined spaces list. If space rooms are
64
    /// joined or left, the stream will yield diffs that reflect the changes.
65
    pub async fn subscribe_to_top_level_joined_spaces(
66
        &self,
67
        listener: Box<dyn SpaceServiceJoinedSpacesListener>,
68
    ) -> Arc<TaskHandle> {
69
        let (initial_values, mut stream) = self.inner.subscribe_to_top_level_joined_spaces().await;
70
71
        listener.on_update(vec![SpaceListUpdate::Reset {
72
            values: initial_values.into_iter().map(Into::into).collect(),
73
        }]);
74
75
        Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
76
            while let Some(diffs) = stream.next().await {
77
                listener.on_update(diffs.into_iter().map(Into::into).collect());
78
            }
79
        })))
80
    }
81
82
    /// Space filters provide access to a custom subset of the space graph that
83
    /// can be used in tandem with the [`crate::RoomListService`] to narrow
84
    /// down the presented rooms.
85
    ///
86
    /// They are limited to the first 2 levels of the graph, with the first
87
    /// level only containing direct descendants while the second holds the rest
88
    /// of them recursively.
89
    pub async fn space_filters(&self) -> Vec<SpaceFilter> {
90
        self.inner.space_filters().await.into_iter().map(|s| s.into()).collect()
91
    }
92
93
    /// Subscribe to changes or updates to the space filters.
94
    pub async fn subscribe_to_space_filters(
95
        &self,
96
        listener: Box<dyn SpaceServiceSpaceFiltersListener>,
97
    ) -> Arc<TaskHandle> {
98
        let (initial_values, mut stream) = self.inner.subscribe_to_space_filters().await;
99
100
        listener.on_update(vec![SpaceFilterUpdate::Reset {
101
            values: initial_values.into_iter().map(Into::into).collect(),
102
        }]);
103
104
        Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
105
            while let Some(diffs) = stream.next().await {
106
                listener.on_update(diffs.into_iter().map(Into::into).collect());
107
            }
108
        })))
109
    }
110
111
    /// Returns a flattened list containing all the spaces where the user has
112
    /// permission to send `m.space.child` state events.
113
    ///
114
    /// Note: Unlike [`Self::top_level_joined_spaces()`], this method does not
115
    /// recompute the space graph, nor does it notify subscribers about changes.
116
    pub async fn editable_spaces(&self) -> Vec<SpaceRoom> {
117
        self.inner.editable_spaces().await.into_iter().map(Into::into).collect()
118
    }
119
120
    /// Returns a `SpaceRoomList` for the given space ID.
121
    pub async fn space_room_list(
122
        &self,
123
        space_id: String,
124
    ) -> Result<Arc<SpaceRoomList>, ClientError> {
125
        let space_id = RoomId::parse(space_id)?;
126
        Ok(Arc::new(SpaceRoomList::new(self.inner.space_room_list(space_id).await)))
127
    }
128
129
    /// Returns all known direct-parents of a given space room ID.
130
    pub async fn joined_parents_of_child(
131
        &self,
132
        child_id: String,
133
    ) -> Result<Vec<SpaceRoom>, ClientError> {
134
        let child_id = RoomId::parse(child_id)?;
135
136
        let parents = self.inner.joined_parents_of_child(&child_id).await;
137
138
        Ok(parents.into_iter().map(Into::into).collect())
139
    }
140
141
    /// Returns the corresponding `SpaceRoom` for the given room ID, or `None`
142
    /// if it isn't known.
143
    pub async fn get_space_room(&self, room_id: String) -> Result<Option<SpaceRoom>, ClientError> {
144
        let room_id = RoomId::parse(room_id.as_str())?;
145
        Ok(self.inner.get_space_room(&room_id).await.map(Into::into))
146
    }
147
148
    pub async fn add_child_to_space(
149
        &self,
150
        child_id: String,
151
        space_id: String,
152
    ) -> Result<(), ClientError> {
153
        let space_id = RoomId::parse(space_id)?;
154
        let child_id = RoomId::parse(child_id)?;
155
156
        self.inner.add_child_to_space(child_id, space_id).await.map_err(ClientError::from)
157
    }
158
159
    pub async fn remove_child_from_space(
160
        &self,
161
        child_id: String,
162
        space_id: String,
163
    ) -> Result<(), ClientError> {
164
        let space_id = RoomId::parse(space_id)?;
165
        let child_id = RoomId::parse(child_id)?;
166
167
        self.inner.remove_child_from_space(child_id, space_id).await.map_err(ClientError::from)
168
    }
169
170
    /// Start a space leave process returning a [`LeaveSpaceHandle`] from which
171
    /// rooms can be retrieved in reversed BFS order starting from the requested
172
    /// `space_id` graph node. If the room is unknown then an error will be
173
    /// returned.
174
    ///
175
    /// Once the rooms to be left are chosen the handle can be used to leave
176
    /// them.
177
    pub async fn leave_space(
178
        &self,
179
        space_id: String,
180
    ) -> Result<Arc<LeaveSpaceHandle>, ClientError> {
181
        let space_id = RoomId::parse(space_id)?;
182
183
        let handle = self.inner.leave_space(&space_id).await.map_err(ClientError::from)?;
184
185
        Ok(Arc::new(handle.into()))
186
    }
187
}
188
189
/// The `SpaceRoomList` represents a paginated list of direct rooms
190
/// that belong to a particular space.
191
///
192
/// It can be used to paginate through the list (and have live updates on the
193
/// pagination state) as well as subscribe to changes as rooms are joined or
194
/// left.
195
///
196
/// The `SpaceRoomList` also automatically subscribes to client room changes
197
/// and updates the list accordingly as rooms are joined or left.
198
#[derive(uniffi::Object)]
199
pub struct SpaceRoomList {
200
    inner: UISpaceRoomList,
201
}
202
203
impl SpaceRoomList {
204
    /// Creates a new `SpaceRoomList` for the underlying UI crate room list.
205
    fn new(inner: UISpaceRoomList) -> Self {
206
        Self { inner }
207
    }
208
}
209
210
#[matrix_sdk_ffi_macros::export]
211
impl SpaceRoomList {
212
    /// Returns the space of the room list if known.
213
    pub fn space(&self) -> Option<SpaceRoom> {
214
        self.inner.space().map(Into::into)
215
    }
216
217
    /// Subscribe to space updates.
218
    pub fn subscribe_to_space_updates(
219
        &self,
220
        listener: Box<dyn SpaceRoomListSpaceListener>,
221
    ) -> Arc<TaskHandle> {
222
        let space_updates = self.inner.subscribe_to_space_updates();
223
224
        Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
225
            pin_mut!(space_updates);
226
227
            while let Some(space) = space_updates.next().await {
228
                listener.on_update(space.map(Into::into));
229
            }
230
        })))
231
    }
232
233
    /// Returns if the room list is currently paginating or not.
234
    pub fn pagination_state(&self) -> SpaceRoomListPaginationState {
235
        self.inner.pagination_state()
236
    }
237
238
    /// Subscribe to pagination updates.
239
    pub fn subscribe_to_pagination_state_updates(
240
        &self,
241
        listener: Box<dyn SpaceRoomListPaginationStateListener>,
242
    ) -> Arc<TaskHandle> {
243
        let pagination_state = self.inner.subscribe_to_pagination_state_updates();
244
245
        Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
246
            pin_mut!(pagination_state);
247
248
            while let Some(state) = pagination_state.next().await {
249
                listener.on_update(state);
250
            }
251
        })))
252
    }
253
254
    /// Return the current list of rooms.
255
    pub async fn rooms(&self) -> Vec<SpaceRoom> {
256
        self.inner.rooms().await.into_iter().map(Into::into).collect()
257
    }
258
259
    /// Subscribes to room list updates.
260
    pub async fn subscribe_to_room_update(
261
        &self,
262
        listener: Box<dyn SpaceRoomListEntriesListener>,
263
    ) -> Arc<TaskHandle> {
264
        let (initial_values, mut stream) = self.inner.subscribe_to_room_updates().await;
265
266
        listener.on_update(vec![SpaceListUpdate::Reset {
267
            values: initial_values.into_iter().map(Into::into).collect(),
268
        }]);
269
270
        Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
271
            while let Some(diffs) = stream.next().await {
272
                listener.on_update(diffs.into_iter().map(Into::into).collect());
273
            }
274
        })))
275
    }
276
277
    /// Ask the list to retrieve the next page if the end hasn't been reached
278
    /// yet. Otherwise it no-ops.
279
    pub async fn paginate(&self) -> Result<(), ClientError> {
280
        self.inner.paginate().await.map_err(ClientError::from)
281
    }
282
283
    /// Clears the room list back to its initial state so that any new changes
284
    /// to the hierarchy will be included the next time [`Self::paginate`] is
285
    /// called.
286
    ///
287
    /// This is useful when you've added or removed children from the space as
288
    /// the list is based on a cached state that lives server-side, meaning
289
    /// the /hierarchy request needs to be restarted from scratch to pick up
290
    /// the changes.
291
    pub async fn reset(&self) {
292
        self.inner.reset().await;
293
    }
294
}
295
296
#[matrix_sdk_ffi_macros::export(callback_interface)]
297
pub trait SpaceRoomListSpaceListener: SendOutsideWasm + SyncOutsideWasm + Debug {
298
    fn on_update(&self, space: Option<SpaceRoom>);
299
}
300
301
#[matrix_sdk_ffi_macros::export(callback_interface)]
302
pub trait SpaceRoomListPaginationStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
303
    fn on_update(&self, pagination_state: SpaceRoomListPaginationState);
304
}
305
306
#[matrix_sdk_ffi_macros::export(callback_interface)]
307
pub trait SpaceRoomListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
308
    fn on_update(&self, rooms: Vec<SpaceListUpdate>);
309
}
310
311
#[matrix_sdk_ffi_macros::export(callback_interface)]
312
pub trait SpaceServiceJoinedSpacesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
313
    fn on_update(&self, room_updates: Vec<SpaceListUpdate>);
314
}
315
316
#[matrix_sdk_ffi_macros::export(callback_interface)]
317
pub trait SpaceServiceSpaceFiltersListener: SendOutsideWasm + SyncOutsideWasm + Debug {
318
    fn on_update(&self, filter_updates: Vec<SpaceFilterUpdate>);
319
}
320
321
/// Structure representing a room in a space and aggregated information
322
/// relevant to the UI layer.
323
#[derive(uniffi::Record)]
324
pub struct SpaceRoom {
325
    /// The ID of the room.
326
    pub room_id: String,
327
    /// The canonical alias of the room, if any.
328
    pub canonical_alias: Option<String>,
329
    /// The room's name from the room state event if received from sync, or one
330
    /// that's been computed otherwise.
331
    pub display_name: String,
332
    /// Room name as defined by the room state event only.
333
    pub raw_name: Option<String>,
334
    /// The topic of the room, if any.
335
    pub topic: Option<String>,
336
    /// The URL for the room's avatar, if one is set.
337
    pub avatar_url: Option<String>,
338
    /// The type of room from `m.room.create`, if any.
339
    pub room_type: RoomType,
340
    /// The number of members joined to the room.
341
    pub num_joined_members: u64,
342
    /// The join rule of the room.
343
    pub join_rule: Option<JoinRule>,
344
    /// Whether the room may be viewed by users without joining.
345
    pub world_readable: Option<bool>,
346
    /// Whether guest users may join the room and participate in it.
347
    pub guest_can_join: bool,
348
349
    /// Whether this room is a direct room.
350
    ///
351
    /// Only set if the room is known to the client otherwise we
352
    /// assume DMs shouldn't be exposed publicly in spaces.
353
    pub is_direct: Option<bool>,
354
    /// The number of children room this has, if a space.
355
    pub children_count: u64,
356
    /// Whether this room is joined, left etc.
357
    pub state: Option<Membership>,
358
    /// A list of room members considered to be heroes.
359
    pub heroes: Option<Vec<RoomHero>>,
360
    /// The via parameters of the room.
361
    pub via: Vec<String>,
362
    /// Whether this room is a DM, if known.
363
    /// Note this value can be calculated following some assumptions and is not
364
    /// guaranteed to be accurate.
365
    pub is_dm: Option<bool>,
366
}
367
368
impl From<UISpaceRoom> for SpaceRoom {
369
    fn from(room: UISpaceRoom) -> Self {
370
        Self {
371
            room_id: room.room_id.into(),
372
            canonical_alias: room.canonical_alias.map(|alias| alias.into()),
373
            display_name: room.display_name,
374
            raw_name: room.name,
375
            topic: room.topic,
376
            avatar_url: room.avatar_url.map(|url| url.into()),
377
            room_type: room.room_type.into(),
378
            num_joined_members: room.num_joined_members,
379
            join_rule: room.join_rule.map(Into::into),
380
            world_readable: room.world_readable,
381
            guest_can_join: room.guest_can_join,
382
            is_direct: room.is_direct,
383
            children_count: room.children_count,
384
            state: room.state.map(Into::into),
385
            heroes: room.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()),
386
            via: room.via.into_iter().map(Into::into).collect(),
387
            is_dm: room.is_dm,
388
        }
389
    }
390
}
391
392
#[derive(uniffi::Enum)]
393
pub enum SpaceListUpdate {
394
    Append { values: Vec<SpaceRoom> },
395
    Clear,
396
    PushFront { value: SpaceRoom },
397
    PushBack { value: SpaceRoom },
398
    PopFront,
399
    PopBack,
400
    Insert { index: u32, value: SpaceRoom },
401
    Set { index: u32, value: SpaceRoom },
402
    Remove { index: u32 },
403
    Truncate { length: u32 },
404
    Reset { values: Vec<SpaceRoom> },
405
}
406
407
impl From<VectorDiff<UISpaceRoom>> for SpaceListUpdate {
408
    fn from(diff: VectorDiff<UISpaceRoom>) -> Self {
409
        match diff {
410
            VectorDiff::Append { values } => {
411
                Self::Append { values: values.into_iter().map(|v| v.into()).collect() }
412
            }
413
            VectorDiff::Clear => Self::Clear,
414
            VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
415
            VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
416
            VectorDiff::PopFront => Self::PopFront,
417
            VectorDiff::PopBack => Self::PopBack,
418
            VectorDiff::Insert { index, value } => {
419
                Self::Insert { index: index as u32, value: value.into() }
420
            }
421
            VectorDiff::Set { index, value } => {
422
                Self::Set { index: index as u32, value: value.into() }
423
            }
424
            VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
425
            VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
426
            VectorDiff::Reset { values } => {
427
                Self::Reset { values: values.into_iter().map(|v| v.into()).collect() }
428
            }
429
        }
430
    }
431
}
432
433
#[derive(uniffi::Enum)]
434
pub enum SpaceFilterUpdate {
435
    Append { values: Vec<SpaceFilter> },
436
    Clear,
437
    PushFront { value: SpaceFilter },
438
    PushBack { value: SpaceFilter },
439
    PopFront,
440
    PopBack,
441
    Insert { index: u32, value: SpaceFilter },
442
    Set { index: u32, value: SpaceFilter },
443
    Remove { index: u32 },
444
    Truncate { length: u32 },
445
    Reset { values: Vec<SpaceFilter> },
446
}
447
448
impl From<VectorDiff<UISpaceFilter>> for SpaceFilterUpdate {
449
    fn from(diff: VectorDiff<UISpaceFilter>) -> Self {
450
        match diff {
451
            VectorDiff::Append { values } => {
452
                Self::Append { values: values.into_iter().map(|v| v.into()).collect() }
453
            }
454
            VectorDiff::Clear => Self::Clear,
455
            VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
456
            VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
457
            VectorDiff::PopFront => Self::PopFront,
458
            VectorDiff::PopBack => Self::PopBack,
459
            VectorDiff::Insert { index, value } => {
460
                Self::Insert { index: index as u32, value: value.into() }
461
            }
462
            VectorDiff::Set { index, value } => {
463
                Self::Set { index: index as u32, value: value.into() }
464
            }
465
            VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
466
            VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
467
            VectorDiff::Reset { values } => {
468
                Self::Reset { values: values.into_iter().map(|v| v.into()).collect() }
469
            }
470
        }
471
    }
472
}
473
474
/// The `LeaveSpaceHandle` processes rooms to be left in the order they were
475
/// provided by the [`SpaceService`] and annotates them with extra data to
476
/// inform the leave process e.g. if the current user is the last room admin.
477
///
478
/// Once the upstream client decides what rooms should actually be left, the
479
/// handle provides a method to execute that too.
480
#[derive(uniffi::Object)]
481
pub struct LeaveSpaceHandle {
482
    inner: UILeaveSpaceHandle,
483
}
484
485
#[matrix_sdk_ffi_macros::export]
486
impl LeaveSpaceHandle {
487
    /// A list of rooms to be left which next to normal [`SpaceRoom`] data also
488
    /// include leave specific information.
489
    pub fn rooms(&self) -> Vec<LeaveSpaceRoom> {
490
        let rooms = self.inner.rooms();
491
        rooms.iter().map(|room| room.clone().into()).collect()
492
    }
493
494
    /// Bulk leave the given rooms. Stops when encountering an error.
495
    pub async fn leave(&self, room_ids: Vec<String>) -> Result<(), ClientError> {
496
        let room_ids = room_ids.iter().map(RoomId::parse).collect::<Result<Vec<_>, _>>()?;
497
498
        self.inner
499
            .leave(|room| room_ids.contains(&room.space_room.room_id))
500
            .await
501
            .map_err(ClientError::from)
502
    }
503
}
504
505
impl From<UILeaveSpaceHandle> for LeaveSpaceHandle {
506
    fn from(handle: UILeaveSpaceHandle) -> Self {
507
        LeaveSpaceHandle { inner: handle }
508
    }
509
}
510
511
/// Space leaving specific room that groups normal [`SpaceRoom`] details with
512
/// information about the leaving user's role.
513
#[derive(uniffi::Record)]
514
pub struct LeaveSpaceRoom {
515
    /// The underlying [`SpaceRoom`]
516
    pub space_room: SpaceRoom,
517
    /// Whether the user is the last owner in the room. This helps clients
518
    /// better inform the user about the consequences of leaving the room.
519
    pub is_last_owner: bool,
520
    /// If the room creators have infinite PL.
521
    pub are_creators_privileged: bool,
522
}
523
524
impl From<UILeaveSpaceRoom> for LeaveSpaceRoom {
525
    fn from(room: UILeaveSpaceRoom) -> Self {
526
        LeaveSpaceRoom {
527
            space_room: room.space_room.into(),
528
            is_last_owner: room.is_last_owner,
529
            are_creators_privileged: room.are_creators_privileged,
530
        }
531
    }
532
}
533
534
#[derive(uniffi::Record)]
535
pub struct SpaceFilter {
536
    /// The underlying [`SpaceRoom`]
537
    space_room: SpaceRoom,
538
    /// The level of the space filter in the tree/hierarchy.
539
    /// At this point in time the filters are limited to the first 2 levels.
540
    level: u8,
541
    /// The room identifiers of the descendants of this space.
542
    /// For top level spaces (level 0) these will be direct descendants while
543
    /// for first level spaces they will be all other descendants, recursively.
544
    descendants: Vec<String>,
545
}
546
547
impl From<UISpaceFilter> for SpaceFilter {
548
    fn from(filter: UISpaceFilter) -> Self {
549
        SpaceFilter {
550
            space_room: filter.space_room.into(),
551
            level: filter.level,
552
            descendants: filter.descendants.into_iter().map(|id| id.to_string()).collect(),
553
        }
554
    }
555
}