mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-08-02 19:12:53 -04:00
feat(ui): Add active call information to the current notification item (#6668)
This commit is contained in:
2
bindings/matrix-sdk-ffi/changelog.d/6668.feature
Normal file
2
bindings/matrix-sdk-ffi/changelog.d/6668.feature
Normal file
@@ -0,0 +1,2 @@
|
||||
The `TimelineItemContent::RtcNotification` now contains additional fields for when the notification is related to
|
||||
and active call. The new fields are `active_members` (if not empty then the call is active), `call_start_ts_millis`, `is_joined`.
|
||||
@@ -15,6 +15,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::room::power_levels::power_level_user_changes;
|
||||
use matrix_sdk_base::CallIntentConsensus;
|
||||
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
|
||||
use ruma::events::{
|
||||
StateEventContentChange, room::history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
@@ -40,12 +41,31 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
|
||||
Content::CallInvite => TimelineItemContent::CallInvite,
|
||||
|
||||
Content::RtcNotification { call_intent, declined_by: declinations } => {
|
||||
TimelineItemContent::RtcNotification {
|
||||
call_intent: call_intent.map(|s| s.to_string()),
|
||||
declined_by: declinations.iter().map(|u| u.to_string()).collect(),
|
||||
}
|
||||
}
|
||||
Content::RtcNotification {
|
||||
call_intent,
|
||||
declined_by: declinations,
|
||||
active_call_info,
|
||||
} => TimelineItemContent::RtcNotification {
|
||||
// if the call is active use the live intent
|
||||
call_intent: active_call_info
|
||||
.as_ref()
|
||||
.and_then(|a| match &a.call_intent {
|
||||
CallIntentConsensus::Full(intent) => Some(intent),
|
||||
CallIntentConsensus::Partial { intent, .. } => Some(intent),
|
||||
CallIntentConsensus::None => None,
|
||||
})
|
||||
.or(call_intent.as_ref())
|
||||
.map(|c| c.to_string()),
|
||||
declined_by: declinations.iter().map(|u| u.to_string()).collect(),
|
||||
active_members: active_call_info
|
||||
.as_ref()
|
||||
.map(|a| a.active_members.iter().map(|u| u.to_string()).collect::<Vec<_>>())
|
||||
.unwrap_or_default(),
|
||||
is_joined: active_call_info.as_ref().map(|a| a.is_joined).unwrap_or(false),
|
||||
call_start_ts_millis: active_call_info
|
||||
.and_then(|a| a.call_started_ts_millis)
|
||||
.map(|it| it.0.into()),
|
||||
},
|
||||
Content::MembershipChange(membership) => {
|
||||
let reason = match membership.content() {
|
||||
StateEventContentChange::Original { content, .. } => content.reason.clone(),
|
||||
@@ -166,6 +186,9 @@ pub enum TimelineItemContent {
|
||||
RtcNotification {
|
||||
call_intent: Option<String>,
|
||||
declined_by: Vec<String>,
|
||||
active_members: Vec<String>,
|
||||
call_start_ts_millis: Option<u64>,
|
||||
is_joined: bool,
|
||||
},
|
||||
RoomMembership {
|
||||
user_id: String,
|
||||
|
||||
3
crates/matrix-sdk-ui/changelog.d/6668.feature
Normal file
3
crates/matrix-sdk-ui/changelog.d/6668.feature
Normal file
@@ -0,0 +1,3 @@
|
||||
The `TimelineItemContent::RtcNotification` now contains an optional `ActiveCallInfo` struct, which provides information
|
||||
about the active call associated with the notification.
|
||||
This allows to render in the timeline more detailed context about the active call (members, join state, etc...)
|
||||
@@ -26,8 +26,11 @@ use super::{
|
||||
use crate::{
|
||||
timeline::{
|
||||
TimelineReadReceiptTracking,
|
||||
controller::{InitFocusResult, spawn_crypto_tasks},
|
||||
tasks::{room_event_cache_updates_task, room_send_queue_update_task},
|
||||
controller::{ActiveCallInfo, InitFocusResult, spawn_crypto_tasks},
|
||||
tasks::{
|
||||
room_event_cache_updates_task, room_send_queue_update_task, rtc_membership_update_task,
|
||||
},
|
||||
traits::RoomDataProvider,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
@@ -174,6 +177,9 @@ impl TimelineBuilder {
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let initial_info = room.clone_info();
|
||||
let owned_user_id = room.own_user_id().to_owned();
|
||||
|
||||
let controller = TimelineController::new(
|
||||
room.clone(),
|
||||
&focus,
|
||||
@@ -237,6 +243,28 @@ impl TimelineBuilder {
|
||||
.abort_on_drop()
|
||||
};
|
||||
|
||||
let initial_active_call_info = ActiveCallInfo::from_info(initial_info, owned_user_id);
|
||||
if initial_active_call_info.is_some() {
|
||||
controller.handle_active_call_update(initial_active_call_info).await;
|
||||
}
|
||||
let rtc_membership_listener_handle = {
|
||||
let room_info_subscriber = room.subscribe_info();
|
||||
room.client()
|
||||
.task_monitor()
|
||||
.spawn_infinite_task("timeline::rtc_membership_listener", {
|
||||
let span = info_span!(
|
||||
parent: Span::none(),
|
||||
"rtc_membership_handler",
|
||||
room_id = ?room.room_id(),
|
||||
);
|
||||
span.follows_from(Span::current());
|
||||
|
||||
rtc_membership_update_task(room_info_subscriber, controller.clone())
|
||||
.instrument(span)
|
||||
})
|
||||
.abort_on_drop()
|
||||
};
|
||||
|
||||
let crypto_drop_handles = spawn_crypto_tasks(controller.clone()).await;
|
||||
|
||||
let timeline = Timeline {
|
||||
@@ -245,6 +273,7 @@ impl TimelineBuilder {
|
||||
_crypto_drop_handles: crypto_drop_handles,
|
||||
_room_update_join_handle: room_update_join_handle,
|
||||
_local_echo_listener_handle: local_echo_listener_handle,
|
||||
_rtc_membership_listener_handle: rtc_membership_listener_handle,
|
||||
_focus_drop_handle: focus_task,
|
||||
_event_cache_drop_handle: event_cache_drop,
|
||||
}),
|
||||
|
||||
@@ -35,8 +35,8 @@ use tracing::trace;
|
||||
|
||||
use super::{
|
||||
super::{TimelineItem, TimelineItemKind, TimelineUniqueId, subscriber::skip::SkipCount},
|
||||
Aggregation, AggregationKind, Aggregations, AllRemoteEvents, ObservableItemsTransaction,
|
||||
PendingEdit, PendingEditKind,
|
||||
ActiveCallInfo, Aggregation, AggregationKind, Aggregations, AllRemoteEvents,
|
||||
ObservableItemsTransaction, PendingEdit, PendingEditKind,
|
||||
read_receipts::ReadReceipts,
|
||||
};
|
||||
use crate::{
|
||||
@@ -127,6 +127,24 @@ pub(in crate::timeline) struct TimelineMetadata {
|
||||
///
|
||||
/// TODO: move this over to the event cache (see also #3058).
|
||||
pub(super) read_receipts: ReadReceipts,
|
||||
|
||||
/// The event ID of the active RtcNotification item that should have
|
||||
/// active_members populated.
|
||||
///
|
||||
/// There is no real link to the active room call and a rtc notification
|
||||
/// event. For example there could be several rtc notifications events for
|
||||
/// the same call, but we only want to have a single active call tile in
|
||||
/// the timeline. We achieve this by keeping a link to the latest
|
||||
/// notification event in the timeline and the `active_call` info will
|
||||
/// be attached to it.
|
||||
pub(crate) active_rtc_notification_event_id: Option<OwnedEventId>,
|
||||
|
||||
/// Current active call info for the room.
|
||||
///
|
||||
/// This info is updated everytime the active call membership and intent
|
||||
/// change. It is cached to be attached dynamically to the latest
|
||||
/// RtcNotification event inserted in the timeline.
|
||||
pub(crate) active_call: Option<ActiveCallInfo>,
|
||||
}
|
||||
|
||||
impl TimelineMetadata {
|
||||
@@ -152,9 +170,15 @@ impl TimelineMetadata {
|
||||
unable_to_decrypt_hook,
|
||||
internal_id_prefix,
|
||||
is_room_encrypted,
|
||||
active_rtc_notification_event_id: None,
|
||||
active_call: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn with_active_call_info(self, active_call_info: Option<ActiveCallInfo>) -> Self {
|
||||
Self { active_call: active_call_info, ..self }
|
||||
}
|
||||
|
||||
pub(super) fn clear(&mut self) {
|
||||
// Note: we don't clear the next internal id to avoid bad cases of stale unique
|
||||
// ids across timeline clears.
|
||||
|
||||
@@ -23,7 +23,7 @@ use as_variant::as_variant;
|
||||
use eyeball_im::{VectorDiff, VectorSubscriberStream};
|
||||
use eyeball_im_util::vector::{FilterMap, VectorObserverExt};
|
||||
use futures_core::Stream;
|
||||
use imbl::Vector;
|
||||
use imbl::{HashSet, Vector};
|
||||
use matrix_sdk::{
|
||||
deserialized_responses::TimelineEvent,
|
||||
event_cache::{
|
||||
@@ -38,7 +38,8 @@ use matrix_sdk::{
|
||||
#[cfg(test)]
|
||||
use ruma::events::receipt::ReceiptEventContent;
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, TransactionId, UserId,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId,
|
||||
TransactionId, UserId,
|
||||
api::client::receipt::create_receipt::v3::ReceiptType as SendReceiptType,
|
||||
events::{
|
||||
AnyMessageLikeEventContent, AnySyncEphemeralRoomEvent, AnySyncMessageLikeEvent,
|
||||
@@ -99,6 +100,7 @@ mod state_transaction;
|
||||
|
||||
pub(super) use aggregations::*;
|
||||
pub(super) use decryption_retry_task::{CryptoDropHandles, spawn_crypto_tasks};
|
||||
use matrix_sdk_base::{CallIntentConsensus, RoomInfo};
|
||||
|
||||
/// Data associated to the current timeline focus.
|
||||
///
|
||||
@@ -353,6 +355,41 @@ pub(super) struct InitFocusResult {
|
||||
pub focus_task: Option<BackgroundTaskHandle>,
|
||||
}
|
||||
|
||||
/// Holds the various info about the current call
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ActiveCallInfo {
|
||||
/// The list of users in the call
|
||||
pub active_members: HashSet<OwnedUserId>,
|
||||
/// The consensus intent of the call, audio/video
|
||||
pub call_intent: CallIntentConsensus,
|
||||
/// True if the user (with any device) is currently in the call, meaning
|
||||
/// they have joined and haven't left yet.
|
||||
pub is_joined: bool,
|
||||
/// The timestamp of when the call started, in milliseconds since the unix
|
||||
/// epoch. Currently, this is the origin_server_ts of the rtc.notification
|
||||
/// event.
|
||||
pub call_started_ts_millis: Option<MilliSecondsSinceUnixEpoch>,
|
||||
}
|
||||
|
||||
impl ActiveCallInfo {
|
||||
pub(crate) fn from_info(room_info: RoomInfo, owned_user_id: OwnedUserId) -> Option<Self> {
|
||||
if room_info.has_active_room_call() {
|
||||
Some(ActiveCallInfo {
|
||||
active_members: HashSet::from(room_info.active_room_call_participants()),
|
||||
call_intent: room_info.active_room_call_consensus_intent(),
|
||||
is_joined: room_info.active_room_call_participants().contains(&owned_user_id),
|
||||
call_started_ts_millis: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_start_time(self, timestamp: Option<MilliSecondsSinceUnixEpoch>) -> Self {
|
||||
Self { call_started_ts_millis: timestamp, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: RoomDataProvider> TimelineController<P> {
|
||||
pub(super) async fn new(
|
||||
room_data_provider: P,
|
||||
@@ -402,6 +439,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
|
||||
internal_id_prefix,
|
||||
unable_to_decrypt_hook,
|
||||
is_room_encrypted,
|
||||
None,
|
||||
)));
|
||||
|
||||
Ok(Self { state, focus, room_data_provider, settings })
|
||||
@@ -752,6 +790,51 @@ impl<P: RoomDataProvider> TimelineController<P> {
|
||||
self.state.write().await.handle_fully_read_marker(fully_read_event_id);
|
||||
}
|
||||
|
||||
pub(super) async fn handle_active_call_update(
|
||||
&self,
|
||||
maybe_active_call: Option<ActiveCallInfo>,
|
||||
) {
|
||||
let mut state = self.state.write().await;
|
||||
let mut txn = state.transaction();
|
||||
|
||||
// Store the current active call info in metadata for new RtcNotification items
|
||||
txn.meta.active_call = maybe_active_call.clone();
|
||||
|
||||
if let Some(existing_event_id) = &txn.meta.active_rtc_notification_event_id {
|
||||
// Clean up the notification event
|
||||
let last_notification = rfind_event_by_id(&txn.items, existing_event_id);
|
||||
if let Some((last_idx, last_notification)) = last_notification {
|
||||
let updated_content = match last_notification.content() {
|
||||
TimelineItemContent::RtcNotification {
|
||||
call_intent,
|
||||
declined_by,
|
||||
active_call_info: _active_call_info,
|
||||
} => Some(TimelineItemContent::RtcNotification {
|
||||
call_intent: call_intent.to_owned(),
|
||||
declined_by: declined_by.clone(),
|
||||
active_call_info: maybe_active_call
|
||||
.clone()
|
||||
.map(|info| info.with_start_time(last_notification.timestamp.into())),
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(new_content) = updated_content {
|
||||
let new_event_item = last_notification.inner.with_content(new_content);
|
||||
let new_timeline_item =
|
||||
TimelineItem::new(new_event_item, last_notification.internal_id.clone());
|
||||
txn.items.replace(last_idx, new_timeline_item);
|
||||
}
|
||||
|
||||
if maybe_active_call.is_none() {
|
||||
// There is no active rtc_notification anymore
|
||||
txn.meta.active_rtc_notification_event_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
|
||||
pub(super) async fn handle_ephemeral_events(
|
||||
&self,
|
||||
events: Vec<Raw<AnySyncEphemeralRoomEvent>>,
|
||||
|
||||
@@ -34,7 +34,7 @@ use super::{
|
||||
event_item::RemoteEventOrigin,
|
||||
traits::RoomDataProvider,
|
||||
},
|
||||
DateDividerMode, TimelineMetadata, TimelineSettings, TimelineStateTransaction,
|
||||
ActiveCallInfo, DateDividerMode, TimelineMetadata, TimelineSettings, TimelineStateTransaction,
|
||||
observable_items::ObservableItems,
|
||||
};
|
||||
use crate::{timeline::controller::TimelineFocusKind, unable_to_decrypt_hook::UtdHookManager};
|
||||
@@ -59,6 +59,7 @@ impl<P: RoomDataProvider> TimelineState<P> {
|
||||
internal_id_prefix: Option<String>,
|
||||
unable_to_decrypt_hook: Option<Arc<UtdHookManager>>,
|
||||
is_room_encrypted: bool,
|
||||
active_call: Option<ActiveCallInfo>,
|
||||
) -> Self {
|
||||
Self {
|
||||
items: ObservableItems::new(),
|
||||
@@ -68,7 +69,8 @@ impl<P: RoomDataProvider> TimelineState<P> {
|
||||
internal_id_prefix,
|
||||
unable_to_decrypt_hook,
|
||||
is_room_encrypted,
|
||||
),
|
||||
)
|
||||
.with_active_call_info(active_call),
|
||||
focus,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
|
||||
@@ -60,7 +60,8 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
timeline::{
|
||||
TimelineUniqueId, controller::aggregations::PendingEdit, event_item::OtherMessageLike,
|
||||
TimelineUniqueId, algorithms::rfind_event_item, controller::aggregations::PendingEdit,
|
||||
event_item::OtherMessageLike,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
@@ -454,6 +455,7 @@ impl TimelineAction {
|
||||
Self::add_item(TimelineItemContent::RtcNotification {
|
||||
call_intent: c.call_intent,
|
||||
declined_by: Vec::new(),
|
||||
active_call_info: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -843,7 +845,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
|
||||
let sender = &self.ctx.sender;
|
||||
|
||||
// Find the live start item by sender and matching content.
|
||||
let target_event_id = super::algorithms::rfind_event_item(self.items, |item| {
|
||||
let target_event_id = rfind_event_item(self.items, |item| {
|
||||
item.sender() == sender
|
||||
&& item.content().as_live_location_state().is_some_and(|s| s.matches_stop(&content))
|
||||
})
|
||||
@@ -1004,6 +1006,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
|
||||
.map(|_| TimelineDetails::from_initial_value(self.ctx.forwarder_profile.clone()));
|
||||
|
||||
let timestamp = self.ctx.timestamp;
|
||||
let is_rtc_notification = matches!(content, TimelineItemContent::RtcNotification { .. });
|
||||
|
||||
let kind: EventTimelineItemKind = match &self.ctx.flow {
|
||||
Flow::Local { txn_id, send_handle } => LocalEventTimelineItem {
|
||||
@@ -1237,12 +1240,92 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle RTC notification active members population and cleanup
|
||||
if is_rtc_notification {
|
||||
self.apply_active_call_to_last_rtc_notification();
|
||||
}
|
||||
|
||||
// If we don't have a read marker item, look if we need to add one now.
|
||||
if !self.meta.has_up_to_date_read_marker_item {
|
||||
self.meta.update_read_marker(self.items);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures that the last RtcNotification in the timeline has the current
|
||||
/// active call info, and cleans up any previous notification that had
|
||||
/// active members.
|
||||
fn apply_active_call_to_last_rtc_notification(&mut self) {
|
||||
let last_notification = rfind_event_item(self.items, |it| {
|
||||
matches!(it.content(), TimelineItemContent::RtcNotification { .. })
|
||||
})
|
||||
.map(|(a, b)| (a, b.internal_id.clone(), b.clone()));
|
||||
|
||||
if let Some((idx, internal_id, last_notification)) = last_notification {
|
||||
// Is this the same as previously?
|
||||
if let Some(prev_event_id) = &self.meta.active_rtc_notification_event_id {
|
||||
if Some(prev_event_id.as_ref()) == last_notification.event_id() {
|
||||
// then no changes, the newest rtc_notification event have not change
|
||||
return;
|
||||
}
|
||||
|
||||
// They are different, clean the old event
|
||||
// Find the previous notification item and clear its active_members
|
||||
if let Some((idx, prev_item)) =
|
||||
rfind_event_item(self.items, |it: &EventTimelineItem| {
|
||||
it.event_id() == Some(prev_event_id)
|
||||
})
|
||||
&& let TimelineItemContent::RtcNotification {
|
||||
call_intent,
|
||||
declined_by,
|
||||
active_call_info,
|
||||
} = prev_item.content()
|
||||
{
|
||||
// Only clear if it has active members (not already empty)
|
||||
if active_call_info.is_some() {
|
||||
let new_content = TimelineItemContent::RtcNotification {
|
||||
call_intent: call_intent.clone(),
|
||||
declined_by: declined_by.clone(),
|
||||
active_call_info: None,
|
||||
};
|
||||
let new_event_item = prev_item.inner.with_content(new_content);
|
||||
let new_timeline_item =
|
||||
TimelineItem::new(new_event_item, prev_item.internal_id.clone());
|
||||
self.items.replace(idx, new_timeline_item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the new last notification if needed
|
||||
if self.meta.active_call.is_some() {
|
||||
let TimelineItemContent::RtcNotification { call_intent, declined_by, .. } =
|
||||
last_notification.content()
|
||||
else {
|
||||
// Nothing to update
|
||||
return;
|
||||
};
|
||||
|
||||
let new_content = TimelineItemContent::RtcNotification {
|
||||
call_intent: call_intent.clone(),
|
||||
declined_by: declined_by.clone(),
|
||||
active_call_info: self
|
||||
.meta
|
||||
.active_call
|
||||
.clone()
|
||||
.map(|c| c.with_start_time(Some(self.ctx.timestamp))),
|
||||
};
|
||||
|
||||
let updated_event_item = last_notification.with_content(new_content);
|
||||
let new_timeline_item = TimelineItem::new(updated_event_item, internal_id);
|
||||
self.items.replace(idx, new_timeline_item);
|
||||
|
||||
// Update the active_rtc_notification_event_id so that this event gets any new
|
||||
// updates of call membership
|
||||
self.meta.active_rtc_notification_event_id =
|
||||
last_notification.event_id().map(ToOwned::to_owned);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to recycle a local timeline item for the same event, or create a new
|
||||
/// timeline item for it.
|
||||
///
|
||||
|
||||
@@ -80,7 +80,10 @@ pub use self::{
|
||||
reply::{EmbeddedEvent, InReplyToDetails},
|
||||
};
|
||||
use super::ReactionsByKeyBySender;
|
||||
use crate::timeline::event_handler::{HandleAggregationKind, TimelineAction};
|
||||
use crate::timeline::{
|
||||
controller::ActiveCallInfo,
|
||||
event_handler::{HandleAggregationKind, TimelineAction},
|
||||
};
|
||||
|
||||
/// The content of an [`EventTimelineItem`][super::EventTimelineItem].
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -127,6 +130,9 @@ pub enum TimelineItemContent {
|
||||
call_intent: Option<CallIntent>,
|
||||
/// Users who have declined this call notification
|
||||
declined_by: Vec<OwnedUserId>,
|
||||
/// Information about the active call, if this notification is about an
|
||||
/// active call.
|
||||
active_call_info: Option<ActiveCallInfo>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -886,6 +886,7 @@ impl Timeline {
|
||||
struct TimelineDropHandle {
|
||||
_room_update_join_handle: BackgroundTaskHandle,
|
||||
_local_echo_listener_handle: BackgroundTaskHandle,
|
||||
_rtc_membership_listener_handle: BackgroundTaskHandle,
|
||||
_event_cache_drop_handle: Arc<EventCacheDropHandles>,
|
||||
_focus_drop_handle: Option<BackgroundTaskHandle>,
|
||||
_crypto_drop_handles: CryptoDropHandles,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use eyeball::Subscriber as EyeballSubscriber;
|
||||
use matrix_sdk::{
|
||||
event_cache::{
|
||||
EventFocusThreadMode, EventFocusedCache, EventsOrigin, PinnedEventsCache, RoomEventCache,
|
||||
@@ -23,11 +24,15 @@ use matrix_sdk::{
|
||||
},
|
||||
send_queue::RoomSendQueueUpdate,
|
||||
};
|
||||
use matrix_sdk_base::RoomInfo;
|
||||
use ruma::OwnedEventId;
|
||||
use tokio::sync::broadcast::{Receiver, error::RecvError};
|
||||
use tracing::{error, instrument, trace, warn};
|
||||
|
||||
use crate::timeline::{TimelineController, TimelineFocus, event_item::RemoteEventOrigin};
|
||||
use crate::timeline::{
|
||||
TimelineController, TimelineFocus, controller::ActiveCallInfo, event_item::RemoteEventOrigin,
|
||||
traits::RoomDataProvider,
|
||||
};
|
||||
|
||||
/// Long-lived task, in the pinned events focus mode, that updates the timeline
|
||||
/// after any changes in the pinned events.
|
||||
@@ -309,3 +314,23 @@ pub(in crate::timeline) async fn room_send_queue_update_task(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Long-lived task that watches RoomInfo for RTC membership changes
|
||||
/// and updates the active RtcNotification timeline item.
|
||||
pub(in crate::timeline) async fn rtc_membership_update_task(
|
||||
mut room_info: EyeballSubscriber<RoomInfo>,
|
||||
timeline_controller: TimelineController,
|
||||
) {
|
||||
let mut prev_info = None;
|
||||
let own_user = timeline_controller.room().own_user_id().to_owned();
|
||||
|
||||
while let Some(info) = room_info.next().await {
|
||||
let active_call = ActiveCallInfo::from_info(info, own_user.clone());
|
||||
// RoomInfo fires for many reasons; only act when the participant
|
||||
// list actually changed.
|
||||
if active_call != prev_info {
|
||||
prev_info = active_call.clone();
|
||||
timeline_controller.handle_active_call_update(active_call).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ mod polls;
|
||||
mod reactions;
|
||||
mod read_receipts;
|
||||
mod redaction;
|
||||
mod rtc;
|
||||
mod shields;
|
||||
mod virt;
|
||||
|
||||
|
||||
230
crates/matrix-sdk-ui/src/timeline/tests/rtc.rs
Normal file
230
crates/matrix-sdk-ui/src/timeline/tests/rtc.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
//! Tests for RTC notification timeline items.
|
||||
|
||||
use std::ops::Add;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use eyeball_im::VectorDiff;
|
||||
use matrix_sdk_base::CallIntentConsensus;
|
||||
use matrix_sdk_test::{ALICE, BOB, CAROL};
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::rtc::notification::{CallIntent, NotificationType},
|
||||
};
|
||||
use stream_assert::{assert_next_matches, assert_pending};
|
||||
|
||||
use super::*;
|
||||
use crate::timeline::{TimelineItemContent, controller::ActiveCallInfo};
|
||||
|
||||
/// Test that `handle_rtc_members_update` correctly updates the active_members
|
||||
/// of the RtcNotification timeline item.
|
||||
#[tokio::test]
|
||||
async fn test_rtc_members_update() {
|
||||
let timeline = TestTimeline::new().await;
|
||||
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
|
||||
let f = &timeline.factory;
|
||||
|
||||
set_active_call_members(&timeline, &[BOB.to_owned()]).await;
|
||||
|
||||
let notification_event = f
|
||||
.rtc_notification(NotificationType::Ring)
|
||||
.sender(*ALICE)
|
||||
.call_intent(CallIntent::Audio)
|
||||
.event_id(event_id!("$notification_event"));
|
||||
|
||||
timeline.handle_live_event(notification_event).await;
|
||||
|
||||
// This is two steps, first the item is added, then it is updated with the
|
||||
// active members
|
||||
assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
let notification_item =
|
||||
assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
|
||||
assert_rtc_notification_active_members(notification_item, vec![BOB.to_owned()]);
|
||||
|
||||
// Simulate two members joined the call
|
||||
set_active_call_members(&timeline, &[BOB.to_owned(), CAROL.to_owned()]).await;
|
||||
|
||||
let notification_item = assert_next_matches!(stream, VectorDiff::Set { value, .. } => value);
|
||||
assert_rtc_notification_active_members(
|
||||
notification_item,
|
||||
vec![BOB.to_owned(), CAROL.to_owned()],
|
||||
);
|
||||
|
||||
// Simulate CAROL left
|
||||
set_active_call_members(&timeline, &[BOB.to_owned()]).await;
|
||||
|
||||
let notification_item = assert_next_matches!(stream, VectorDiff::Set { value, .. } => value);
|
||||
assert_rtc_notification_active_members(notification_item, vec![BOB.to_owned()]);
|
||||
}
|
||||
|
||||
/// Test that `handle_rtc_members_update` correctly updates the active_members
|
||||
/// of the last notification event
|
||||
#[tokio::test]
|
||||
async fn test_rtc_members_update_last_only() {
|
||||
let timeline = TestTimeline::new().await;
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
|
||||
let f = &timeline.factory;
|
||||
|
||||
let some_timestamp = SystemTime::now();
|
||||
|
||||
// ===========
|
||||
// ACT: Simulate active members in the call
|
||||
// ===========
|
||||
set_active_call_members(&timeline, &[BOB.to_owned()]).await;
|
||||
|
||||
// ===========
|
||||
// ACT: Insert a first notification event
|
||||
// ===========
|
||||
timeline
|
||||
.handle_live_event(
|
||||
f.rtc_notification(NotificationType::Ring)
|
||||
.sender(*ALICE)
|
||||
.call_intent(CallIntent::Audio)
|
||||
.event_id(event_id!("$notification_event_old"))
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::from_system_time(some_timestamp).unwrap()),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_next_matches!(stream, VectorDiff::PushBack { .. });
|
||||
|
||||
// ===========
|
||||
// ASSERT: The notification item should be updated with the active members
|
||||
// ===========
|
||||
|
||||
let notification_item = assert_next_matches!(stream, VectorDiff::Set { value, .. } => value);
|
||||
let info = assert_rtc_notification_active_members(notification_item, vec![BOB.to_owned()]);
|
||||
assert_eq!(
|
||||
info.call_started_ts_millis,
|
||||
Some(MilliSecondsSinceUnixEpoch::from_system_time(some_timestamp).unwrap())
|
||||
);
|
||||
|
||||
// ===========
|
||||
// ACT: Insert a new notification event with a newer timestamp.
|
||||
// ===========
|
||||
let new_notification_ts = some_timestamp.add(Duration::from_secs(60));
|
||||
|
||||
timeline
|
||||
.handle_live_event(
|
||||
f.rtc_notification(NotificationType::Ring)
|
||||
.sender(*ALICE)
|
||||
.call_intent(CallIntent::Audio)
|
||||
.event_id(event_id!("$notification_event_new"))
|
||||
.server_ts(
|
||||
MilliSecondsSinceUnixEpoch::from_system_time(new_notification_ts).unwrap(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
// ===========
|
||||
// ASSERT: As per requirement only the latest notification item in the timeline
|
||||
// should have the active call info. So ensure the oldest notification item
|
||||
// is cleared and that the new one has the previously known info
|
||||
// ===========
|
||||
|
||||
// The notification is first added
|
||||
assert_next_matches!(
|
||||
stream,
|
||||
VectorDiff::PushBack { value } => value
|
||||
);
|
||||
|
||||
// The old one is cleared
|
||||
let old_notif_updated = assert_next_matches!(
|
||||
stream,
|
||||
VectorDiff::Set { index: 0, value } => value
|
||||
);
|
||||
assert_eq!(
|
||||
old_notif_updated.event_id().expect("eventId should be set"),
|
||||
event_id!("$notification_event_old")
|
||||
);
|
||||
assert_matches!(old_notif_updated.content, TimelineItemContent::RtcNotification { active_call_info: None, .. } => {});
|
||||
|
||||
// The new one is updated with the previously known info
|
||||
let item = assert_next_matches!(
|
||||
stream,
|
||||
VectorDiff::Set { index: 1, value } => value
|
||||
);
|
||||
let info = assert_rtc_notification_active_members(item, vec![BOB.to_owned()]);
|
||||
assert_eq!(
|
||||
info.call_started_ts_millis,
|
||||
Some(MilliSecondsSinceUnixEpoch::from_system_time(new_notification_ts).unwrap())
|
||||
);
|
||||
|
||||
// ===========
|
||||
// ACT: Simulate new active members in the call
|
||||
// ===========
|
||||
|
||||
set_active_call_members(&timeline, &[BOB.to_owned(), CAROL.to_owned()]).await;
|
||||
|
||||
// ===========
|
||||
// ASSERT: Should only update the latest notification item.
|
||||
// ===========
|
||||
|
||||
let notification_item = assert_next_matches!(stream, VectorDiff::Set { value, .. } => value);
|
||||
assert_eq!(
|
||||
notification_item.event_id().expect("eventId should be set"),
|
||||
event_id!("$notification_event_new")
|
||||
);
|
||||
assert_rtc_notification_active_members(
|
||||
notification_item,
|
||||
vec![BOB.to_owned(), CAROL.to_owned()],
|
||||
);
|
||||
|
||||
// Simulate back paginated rtc notification
|
||||
|
||||
let old_notification_ts = some_timestamp.sub(Duration::from_secs(60));
|
||||
|
||||
timeline
|
||||
.controller
|
||||
.handle_remote_events_with_diffs(
|
||||
vec![VectorDiff::PushFront {
|
||||
value: f
|
||||
.rtc_notification(NotificationType::Ring)
|
||||
.sender(*ALICE)
|
||||
.call_intent(CallIntent::Audio)
|
||||
.event_id(event_id!("$notification_event_old"))
|
||||
.server_ts(
|
||||
MilliSecondsSinceUnixEpoch::from_system_time(old_notification_ts).unwrap(),
|
||||
)
|
||||
.into_event(),
|
||||
}],
|
||||
RemoteEventOrigin::Pagination,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Should only add this notification, and not modify the most recent one
|
||||
let notification_item =
|
||||
assert_next_matches!(stream, VectorDiff::PushFront { value, .. } => value);
|
||||
assert_eq!(
|
||||
notification_item.event_id().expect("eventId should be set"),
|
||||
event_id!("$notification_event_old")
|
||||
);
|
||||
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_rtc_notification_active_members(
|
||||
item: EventTimelineItem,
|
||||
members: Vec<OwnedUserId>,
|
||||
) -> ActiveCallInfo {
|
||||
assert_matches!(item.content, TimelineItemContent::RtcNotification { active_call_info: Some(active_call_info), .. } => {
|
||||
let active_members = &active_call_info.active_members;
|
||||
assert_eq!(active_members.len(), members.len());
|
||||
for member in members {
|
||||
assert!(active_members.contains(&member));
|
||||
}
|
||||
active_call_info
|
||||
})
|
||||
}
|
||||
|
||||
async fn set_active_call_members(timeline: &TestTimeline, members: &[OwnedUserId]) {
|
||||
let active_call = ActiveCallInfo {
|
||||
active_members: members.to_owned().into(),
|
||||
call_intent: CallIntentConsensus::None,
|
||||
is_joined: false,
|
||||
call_started_ts_millis: MilliSecondsSinceUnixEpoch::now().into(),
|
||||
};
|
||||
timeline.controller.handle_active_call_update(Some(active_call)).await;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use matrix_sdk_test::{
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{RoomExt, TimelineItemContent};
|
||||
use ruma::{
|
||||
event_id,
|
||||
MilliSecondsSinceUnixEpoch, event_id,
|
||||
events::rtc::notification::{CallIntent, NotificationType},
|
||||
room_id,
|
||||
};
|
||||
@@ -54,7 +54,8 @@ async fn test_decline_call() {
|
||||
let event_item = message.as_event().unwrap();
|
||||
assert!(matches!(event_item.content(), TimelineItemContent::RtcNotification { .. }));
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { call_intent: _, declined_by } = event_item.content()
|
||||
TimelineItemContent::RtcNotification { call_intent: _, declined_by, .. } =
|
||||
event_item.content()
|
||||
);
|
||||
assert_eq!(declined_by.len(), 0);
|
||||
|
||||
@@ -64,7 +65,8 @@ async fn test_decline_call() {
|
||||
let event_item = updated_message.as_event().unwrap();
|
||||
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { call_intent, declined_by } = event_item.content()
|
||||
TimelineItemContent::RtcNotification { call_intent, declined_by, .. } =
|
||||
event_item.content()
|
||||
);
|
||||
assert_eq!(declined_by.len(), 1);
|
||||
assert_eq!(declined_by[0], *BOB);
|
||||
@@ -118,7 +120,8 @@ async fn test_multiple_decline_call() {
|
||||
let event_item = message.as_event().unwrap();
|
||||
assert!(matches!(event_item.content(), TimelineItemContent::RtcNotification { .. }));
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { call_intent: _, declined_by } = event_item.content()
|
||||
TimelineItemContent::RtcNotification { call_intent: _, declined_by, .. } =
|
||||
event_item.content()
|
||||
);
|
||||
assert_eq!(declined_by.len(), 0);
|
||||
|
||||
@@ -128,7 +131,8 @@ async fn test_multiple_decline_call() {
|
||||
let event_item = updated_message.as_event().unwrap();
|
||||
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { call_intent: _, declined_by } = event_item.content()
|
||||
TimelineItemContent::RtcNotification { call_intent: _, declined_by, .. } =
|
||||
event_item.content()
|
||||
);
|
||||
assert_eq!(declined_by.len(), 1);
|
||||
assert_eq!(declined_by[0], *BOB);
|
||||
@@ -139,9 +143,281 @@ async fn test_multiple_decline_call() {
|
||||
let event_item = updated_message.as_event().unwrap();
|
||||
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { call_intent: _, declined_by } = event_item.content()
|
||||
TimelineItemContent::RtcNotification { call_intent: _, declined_by, .. } =
|
||||
event_item.content()
|
||||
);
|
||||
assert_eq!(declined_by.len(), 2);
|
||||
assert_eq!(declined_by[0], *BOB);
|
||||
assert_eq!(declined_by[1], *CAROL);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_active_call_info() {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let room = server.sync_joined_room(&client, room_id).await;
|
||||
|
||||
server.mock_room_state_encryption().plain().mount().await;
|
||||
|
||||
let timeline = room.timeline().await.unwrap();
|
||||
let (_, mut timeline_stream) = timeline.subscribe().await;
|
||||
|
||||
let notification_event_id = event_id!("$notify");
|
||||
let bob_membership = event_id!("$bob-joined");
|
||||
let carol_membership = event_id!("$carol-joined");
|
||||
let alice_membership = event_id!("$alice-joined");
|
||||
let notification_timestamp = MilliSecondsSinceUnixEpoch::now();
|
||||
|
||||
let f = EventFactory::new();
|
||||
server
|
||||
.sync_room(
|
||||
&client,
|
||||
JoinedRoomBuilder::new(room_id)
|
||||
.add_timeline_event(
|
||||
f.rtc_notification(NotificationType::Ring)
|
||||
.sender(&ALICE)
|
||||
.server_ts(notification_timestamp)
|
||||
.event_id(notification_event_id),
|
||||
)
|
||||
.add_state_event(
|
||||
f.call_membership_state(BOB.to_owned(), "BOB_DEVICE".to_owned())
|
||||
.set_livekit_focus(room_id.into(), "https://livekit.localhow".to_owned())
|
||||
.event_id(bob_membership),
|
||||
)
|
||||
.add_state_event(
|
||||
f.call_membership_state(CAROL.to_owned(), "CAROL_DEVICE".to_owned())
|
||||
.set_livekit_focus(room_id.into(), "https://livekit.localhow".to_owned())
|
||||
.event_id(carol_membership),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_let_timeout!(Some(timeline_updates) = timeline_stream.next());
|
||||
assert_eq!(timeline_updates.len(), 3);
|
||||
|
||||
// First is adding the notification event
|
||||
assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[0]);
|
||||
let event_item = message.as_event().unwrap();
|
||||
assert!(matches!(event_item.content(), TimelineItemContent::RtcNotification { .. }));
|
||||
|
||||
//Second update is updating it to have active call info
|
||||
assert_let!(VectorDiff::Set { index: 0, value: message } = &timeline_updates[1]);
|
||||
let event_item = message.as_event().unwrap();
|
||||
assert!(matches!(event_item.content(), TimelineItemContent::RtcNotification { .. }));
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { active_call_info: Some(active_call_info), .. } =
|
||||
event_item.content()
|
||||
);
|
||||
let active_members = &active_call_info.active_members;
|
||||
assert_eq!(active_members.len(), 2);
|
||||
assert!(active_members.contains(&BOB.to_owned()));
|
||||
assert!(active_members.contains(&CAROL.to_owned()));
|
||||
assert_eq!(active_call_info.call_started_ts_millis.unwrap(), notification_timestamp);
|
||||
|
||||
let room_info = room.clone_info();
|
||||
assert_eq!(room_info.active_room_call_participants().len(), 2);
|
||||
|
||||
server
|
||||
.sync_room(
|
||||
&client,
|
||||
JoinedRoomBuilder::new(room_id).add_state_event(
|
||||
f.call_membership_state(ALICE.to_owned(), "ALICE_DEVICE".to_owned())
|
||||
.set_livekit_focus(room_id.into(), "https://livekit.localhow".to_owned())
|
||||
.event_id(alice_membership),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_let_timeout!(Some(timeline_updates) = timeline_stream.next());
|
||||
|
||||
assert_let!(VectorDiff::Set { index: 1, value: updated_message } = &timeline_updates[0]);
|
||||
let event_item = updated_message.as_event().unwrap();
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { active_call_info: Some(active_call_info), .. } =
|
||||
event_item.content()
|
||||
);
|
||||
let active_members = &active_call_info.active_members;
|
||||
assert_eq!(active_members.len(), 3);
|
||||
assert!(active_members.contains(&BOB.to_owned()));
|
||||
assert!(active_members.contains(&CAROL.to_owned()));
|
||||
assert!(active_members.contains(&ALICE.to_owned()));
|
||||
|
||||
// Per spec, if there is now a new call notification event, then the active
|
||||
// call info should now be attached to it and remove from the previous one
|
||||
let new_notification_event_id = event_id!("$notify_new");
|
||||
server
|
||||
.sync_room(
|
||||
&client,
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
f.rtc_notification(NotificationType::Ring)
|
||||
.sender(&ALICE)
|
||||
.event_id(new_notification_event_id),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_let_timeout!(Some(timeline_updates) = timeline_stream.next());
|
||||
|
||||
// timeline_updates[0] is read receipt related
|
||||
assert_let!(VectorDiff::Set { index: 1, .. } = &timeline_updates[0]);
|
||||
// Update 1 is adding the new notification
|
||||
assert_let!(VectorDiff::PushBack { .. } = &timeline_updates[1]);
|
||||
|
||||
// Should update the previous notification to clear it and add a new one
|
||||
assert_let!(VectorDiff::Set { index: 1, value: old_notification } = &timeline_updates[2]);
|
||||
let event_item = old_notification.as_event().unwrap();
|
||||
assert_eq!(event_item.event_id().unwrap(), notification_event_id);
|
||||
// Active call info should be None
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { active_call_info: None, .. } = event_item.content()
|
||||
);
|
||||
|
||||
assert_let!(VectorDiff::Set { index: 2, value: new_notification } = &timeline_updates[3]);
|
||||
let event_item = new_notification.as_event().unwrap();
|
||||
assert_eq!(event_item.event_id().unwrap(), new_notification_event_id);
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { active_call_info: Some(active_call_info), .. } =
|
||||
event_item.content()
|
||||
);
|
||||
let active_members = &active_call_info.active_members;
|
||||
assert_eq!(active_members.len(), 3);
|
||||
assert!(active_members.contains(&BOB.to_owned()));
|
||||
assert!(active_members.contains(&CAROL.to_owned()));
|
||||
assert!(active_members.contains(&ALICE.to_owned()));
|
||||
}
|
||||
|
||||
// There is an order in which the initial events are sent, first the membership
|
||||
// event is sent, then the notification event is sent.
|
||||
// When the membership event is received this will trigger an active call info
|
||||
// update and we don't want that this first update is attached to the current
|
||||
// last notification in the timeline as it is not the correct one, the correct
|
||||
// one will come after. If this is not done then there will be a visual glitch
|
||||
// in the timeline where the last rtc_notification will be updated and then
|
||||
// replaced by the correct one (it will "jump")
|
||||
#[async_test]
|
||||
async fn test_only_update_notification_after_it_has_been_marked_as_last() {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let room = server.sync_joined_room(&client, room_id).await;
|
||||
|
||||
server.mock_room_state_encryption().plain().mount().await;
|
||||
|
||||
let timeline = room.timeline().await.unwrap();
|
||||
let (_, mut timeline_stream) = timeline.subscribe().await;
|
||||
|
||||
let f = EventFactory::new();
|
||||
server
|
||||
.sync_room(
|
||||
&client,
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
f.rtc_notification(NotificationType::Ring)
|
||||
.sender(&ALICE)
|
||||
.event_id(event_id!("$pre-existing-notification")),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
// A call is started, so first the membership is added
|
||||
server
|
||||
.sync_room(
|
||||
&client,
|
||||
JoinedRoomBuilder::new(room_id).add_state_event(
|
||||
f.call_membership_state(BOB.to_owned(), "BOB_DEVICE".to_owned())
|
||||
.set_livekit_focus(room_id.into(), "https://livekit.localhow".to_owned())
|
||||
.event_id(event_id!("$bob-joined")),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Ensure that the existing notification timeline item has no info about the new
|
||||
// call
|
||||
assert_let_timeout!(Some(_timeline_updates) = timeline_stream.next());
|
||||
|
||||
let items = timeline.items().await;
|
||||
let event_items: Vec<_> = items.iter().filter_map(|item| item.as_event()).collect();
|
||||
let notification = event_items[0];
|
||||
|
||||
// Assert that active_call_info is none
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { active_call_info: None, .. } =
|
||||
notification.content()
|
||||
);
|
||||
|
||||
// Now the relevant notification is added
|
||||
let f = EventFactory::new();
|
||||
server
|
||||
.sync_room(
|
||||
&client,
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
f.rtc_notification(NotificationType::Ring)
|
||||
.sender(&ALICE)
|
||||
.event_id(event_id!("$call-notification")),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_let_timeout!(Some(_timeline_updates) = timeline_stream.next());
|
||||
|
||||
let items = timeline.items().await;
|
||||
let event_items: Vec<_> = items.iter().filter_map(|item| item.as_event()).collect();
|
||||
let notification = event_items[1];
|
||||
assert_eq!(notification.event_id().unwrap(), event_id!("$call-notification"));
|
||||
|
||||
// Assert that active_call_info is none
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { active_call_info: Some(active_call_info), .. } =
|
||||
notification.content()
|
||||
);
|
||||
assert!(active_call_info.active_members.contains(&BOB.to_owned()));
|
||||
|
||||
// Simulate call ending
|
||||
// A call is started, so first the membership is added
|
||||
server
|
||||
.sync_room(
|
||||
&client,
|
||||
JoinedRoomBuilder::new(room_id).add_state_event(
|
||||
f.call_membership_state(BOB.to_owned(), "BOB_DEVICE".to_owned())
|
||||
.leave()
|
||||
.event_id(event_id!("$bob-joined")),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let items = timeline.items().await;
|
||||
let event_items: Vec<_> = items.iter().filter_map(|item| item.as_event()).collect();
|
||||
let notification = event_items[1];
|
||||
|
||||
assert_eq!(notification.event_id().unwrap(), event_id!("$call-notification"));
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { active_call_info: None, .. } =
|
||||
notification.content()
|
||||
);
|
||||
|
||||
// If there is a new membership, then this is a new call, should not update the
|
||||
// old notification
|
||||
|
||||
server
|
||||
.sync_room(
|
||||
&client,
|
||||
JoinedRoomBuilder::new(room_id).add_state_event(
|
||||
f.call_membership_state(CAROL.to_owned(), "CAROL_DEVICE".to_owned())
|
||||
.set_livekit_focus(room_id.into(), "https://livekit.localhow".to_owned())
|
||||
.event_id(event_id!("$carol-joined")),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let items = timeline.items().await;
|
||||
let event_items: Vec<_> = items.iter().filter_map(|item| item.as_event()).collect();
|
||||
let notification = event_items[1];
|
||||
|
||||
assert_eq!(notification.event_id().unwrap(), event_id!("$call-notification"));
|
||||
assert_let!(
|
||||
TimelineItemContent::RtcNotification { active_call_info: None, .. } =
|
||||
notification.content()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,14 @@ use ruma::{
|
||||
StaticStateEventContent, StrippedStateEvent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
beacon::BeaconEventContent,
|
||||
beacon_info::BeaconInfoEventContent,
|
||||
call::{SessionDescription, invite::CallInviteEventContent},
|
||||
call::{
|
||||
SessionDescription,
|
||||
invite::CallInviteEventContent,
|
||||
member::{
|
||||
ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
|
||||
CallMemberEventContent, CallMemberStateKey, CallScope, Focus, LivekitFocus,
|
||||
},
|
||||
},
|
||||
direct::{DirectEventContent, OwnedDirectUserIdentifier},
|
||||
fully_read::FullyReadEventContent,
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
@@ -95,6 +102,7 @@ use ruma::{
|
||||
tag::{TagEventContent, Tags},
|
||||
typing::TypingEventContent,
|
||||
},
|
||||
owned_device_id,
|
||||
presence::PresenceState,
|
||||
push::Ruleset,
|
||||
room::RoomType,
|
||||
@@ -1467,6 +1475,44 @@ impl EventFactory {
|
||||
self.event(RtcDeclineEventContent::new(notification_event_id))
|
||||
}
|
||||
|
||||
/// Creates a rtc membership state event.
|
||||
///
|
||||
/// ```
|
||||
/// use matrix_sdk_test::event_factory::EventFactory;
|
||||
/// use ruma::{
|
||||
/// events::{SyncStateEvent, call::member::CallMemberEventContent},
|
||||
/// owned_user_id, room_id,
|
||||
/// serde::Raw,
|
||||
/// };
|
||||
///
|
||||
/// let factory = EventFactory::new().room(room_id!("!test:localhost"));
|
||||
///
|
||||
/// let event: Raw<SyncStateEvent<CallMemberEventContent>> = factory
|
||||
/// .call_membership_state(
|
||||
/// owned_user_id!("@alice:localhost"),
|
||||
/// "ABCDEF".to_owned(),
|
||||
/// )
|
||||
/// .lk_focus("alias".to_owned(), "https://livekit2.com".to_owned())
|
||||
/// .into_raw();
|
||||
/// ```
|
||||
pub fn call_membership_state(
|
||||
&self,
|
||||
user_id: OwnedUserId,
|
||||
device_id: String,
|
||||
) -> EventBuilder<CallMemberEventContent> {
|
||||
let event = self.event(CallMemberEventContent::new(
|
||||
Application::Call(CallApplicationContent::new("".to_owned(), CallScope::Room)),
|
||||
owned_device_id!(device_id.clone()),
|
||||
ActiveFocus::Livekit(ActiveLivekitFocus::new()),
|
||||
vec![],
|
||||
Some(MilliSecondsSinceUnixEpoch::now()),
|
||||
Some(Duration::from_secs(3600)),
|
||||
));
|
||||
event
|
||||
.sender(&user_id)
|
||||
.state_key(CallMemberStateKey::new(user_id, device_id.into(), true).as_ref())
|
||||
}
|
||||
|
||||
/// Create a new `m.direct` global account data event.
|
||||
pub fn direct(&self) -> EventBuilder<DirectEventContent> {
|
||||
self.global_account_data(DirectEventContent::default())
|
||||
@@ -1772,6 +1818,35 @@ impl EventBuilder<RtcNotificationEventContent> {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventBuilder<CallMemberEventContent> {
|
||||
/// Sets the livekit focus to the call membership event
|
||||
pub fn set_livekit_focus(mut self, alias: String, service_url: String) -> Self {
|
||||
if let CallMemberEventContent::SessionContent(session_data) = &mut self.content {
|
||||
session_data.foci_preferred =
|
||||
vec![Focus::Livekit(LivekitFocus::new(alias, service_url))];
|
||||
} else {
|
||||
panic!("focus() called on a non-SessionContent call member event");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the application for the call membership event.
|
||||
pub fn application(mut self, call_id: String, scope: CallScope) -> Self {
|
||||
if let CallMemberEventContent::SessionContent(session_data) = &mut self.content {
|
||||
session_data.application =
|
||||
Application::Call(CallApplicationContent::new(call_id, scope));
|
||||
} else {
|
||||
panic!("application() called on a non-SessionContent call member event");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Make the membership event as a membership event.
|
||||
pub fn leave(mut self) -> Self {
|
||||
self.content = CallMemberEventContent::new_empty(None);
|
||||
self
|
||||
}
|
||||
}
|
||||
pub struct ReadReceiptBuilder<'a> {
|
||||
factory: &'a EventFactory,
|
||||
content: ReceiptEventContent,
|
||||
|
||||
Reference in New Issue
Block a user