Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a49c4cd
feat(ui): Add active call information to the current notification item
BillCarsonFr Jun 16, 2026
e780afc
fixup: test compilation
BillCarsonFr Jun 16, 2026
7303a60
fixup: remove unwanted println!
BillCarsonFr Jun 17, 2026
2daa931
fix test, now rtc notification is updated in two steps
BillCarsonFr Jun 17, 2026
e3cfa0e
update changelog entries
BillCarsonFr Jun 17, 2026
d0edb2e
test: Use track_caller for extrated assert util
BillCarsonFr Jun 18, 2026
87d1bbb
Merge branch 'main' into valere/rtc/active_call
BillCarsonFr Jun 18, 2026
cff75f4
post merge fix, timeline::new is async
BillCarsonFr Jun 18, 2026
412b5df
post merge: fix too many args in constructor
BillCarsonFr Jun 19, 2026
720a66b
review: proper use of the event builder framework
BillCarsonFr Jun 29, 2026
d49cc94
fixup: example not compiling
BillCarsonFr Jun 29, 2026
f0ef719
fixup style
BillCarsonFr Jun 29, 2026
e893aeb
review: add back new line
BillCarsonFr Jun 29, 2026
857d1d3
Merge branch 'main' into valere/rtc/active_call
BillCarsonFr Jun 29, 2026
6794d29
fixup: post merge fix conflicting import
BillCarsonFr Jun 30, 2026
ecda844
review: Avoid unnecessary clones and bad unreachable!
BillCarsonFr Jun 30, 2026
dfaee95
review: remove stale comment
BillCarsonFr Jun 30, 2026
56c6fe4
fixup clippy
BillCarsonFr Jun 30, 2026
b0f3fd6
review: clarify comments
BillCarsonFr Jul 6, 2026
e49eecc
fixup clippy
BillCarsonFr Jul 6, 2026
1dd6701
Merge branch 'main' into valere/rtc/active_call
BillCarsonFr Jul 6, 2026
9b56dfe
review: rename `lk_focus` to `set_livekit_focus`
BillCarsonFr Jul 7, 2026
efd443d
Merge branch 'main' into valere/rtc/active_call
BillCarsonFr Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bindings/matrix-sdk-ffi/changelog.d/6668.feature
Original file line number Diff line number Diff line change
@@ -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`.
35 changes: 29 additions & 6 deletions bindings/matrix-sdk-ffi/src/timeline/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions crates/matrix-sdk-ui/changelog.d/6668.feature
Original file line number Diff line number Diff line change
@@ -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...)
33 changes: 31 additions & 2 deletions crates/matrix-sdk-ui/src/timeline/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
}),
Expand Down
28 changes: 26 additions & 2 deletions crates/matrix-sdk-ui/src/timeline/controller/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand Down
87 changes: 85 additions & 2 deletions crates/matrix-sdk-ui/src/timeline/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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,
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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>>,
Expand Down
6 changes: 4 additions & 2 deletions crates/matrix-sdk-ui/src/timeline/controller/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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(),
Expand All @@ -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,
}
Expand Down
Loading
Loading