feat(ui): add UI crate timeline types for handling MSC3489 live location sharing

# Conflicts:
#	crates/matrix-sdk-ui/src/timeline/event_item/mod.rs
#	crates/matrix-sdk-ui/src/timeline/mod.rs
This commit is contained in:
Stefan Ceriu
2026-03-03 08:55:45 +02:00
committed by Stefan Ceriu
parent 17a9ab41e4
commit e61e86c3b4
4 changed files with 186 additions and 14 deletions

View File

@@ -0,0 +1,139 @@
// Copyright 2026 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Timeline item content for live location sharing (MSC3489).
//!
//! Live location sharing uses two event types:
//! - `org.matrix.msc3672.beacon_info` (state event): starts/stops a sharing
//! session and creates the timeline item represented by
//! [`LiveLocationState`].
//! - `org.matrix.msc3672.beacon` (message-like event): periodic location
//! updates that are aggregated onto the parent [`LiveLocationState`] item.
use ruma::{MilliSecondsSinceUnixEpoch, events::beacon_info::BeaconInfoEventContent};
/// A single location update received from a beacon event.
///
/// Created from an `org.matrix.msc3672.beacon` message-like event and
/// aggregated onto the parent [`LiveLocationState`] timeline item.
#[derive(Clone, Debug)]
pub struct BeaconInfo {
/// The geo URI carrying the user's coordinates (e.g.
/// `"geo:51.5008,0.1247;u=35"`).
pub(in crate::timeline) geo_uri: String,
/// Timestamp of this location update (from the beacon event's
/// `org.matrix.msc3488.ts` field).
pub(in crate::timeline) ts: MilliSecondsSinceUnixEpoch,
/// An optional human-readable description of the location.
pub(in crate::timeline) description: Option<String>,
}
impl BeaconInfo {
/// The geo URI of this location update.
pub fn geo_uri(&self) -> &str {
&self.geo_uri
}
/// The timestamp of this location update.
pub fn ts(&self) -> MilliSecondsSinceUnixEpoch {
self.ts
}
/// An optional human-readable description of this location.
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}
/// The state of a live location sharing session.
///
/// Created when a `org.matrix.msc3672.beacon_info` state event is received.
/// Subsequent `org.matrix.msc3672.beacon` message-like events are aggregated
/// onto this item, appending to [`LiveLocationState::locations`].
///
/// When a user stops sharing (a new `beacon_info` with `live: false` arrives)
/// a *separate* timeline item is created for the stop event. The original
/// item's liveness can be checked via [`LiveLocationState::is_live`], which
/// internally checks both the `live` flag and the session timeout.
#[derive(Clone, Debug)]
pub struct LiveLocationState {
/// The content of the `beacon_info` state event that created this item.
pub(in crate::timeline) beacon_info: BeaconInfoEventContent,
/// All location updates aggregated onto this session, kept sorted by
/// timestamp.
pub(in crate::timeline) locations: Vec<BeaconInfo>,
}
impl LiveLocationState {
/// Create a new [`LiveLocationState`] from the given
/// [`BeaconInfoEventContent`].
pub fn new(beacon_info: BeaconInfoEventContent) -> Self {
Self { beacon_info, locations: Vec::new() }
}
/// Add a location update. Keeps the internal list sorted by timestamp so
/// that [`LiveLocationState::latest_location`] always returns the most
/// recent one.
pub(in crate::timeline) fn add_location(&mut self, location: BeaconInfo) {
match self.locations.binary_search_by_key(&location.ts, |l| l.ts) {
Ok(_) => return, // Duplicate timestamp, do nothing.
Err(index) => self.locations.insert(index, location),
}
}
/// Remove the location update with the given timestamp. Used when
/// unapplying an aggregation (e.g. event cache moves an event).
pub(in crate::timeline) fn remove_location(&mut self, ts: MilliSecondsSinceUnixEpoch) {
self.locations.retain(|l| l.ts != ts);
}
/// All accumulated location updates, sorted by timestamp (oldest first).
pub fn locations(&self) -> &[BeaconInfo] {
&self.locations
}
/// The most recent location update, if any have been received.
pub fn latest_location(&self) -> Option<&BeaconInfo> {
self.locations.last()
}
/// Whether this live location share is still active.
///
/// Returns `false` once the `live` flag has been set to `false` **or**
/// the session's timeout has elapsed.
pub fn is_live(&self) -> bool {
self.beacon_info.is_live()
}
/// Update this session with a stop `beacon_info` event (one where
/// `live` is `false`). This replaces the stored content so that
/// [`LiveLocationState::is_live`] will return `false`.
pub(in crate::timeline) fn stop(&mut self, beacon_info: BeaconInfoEventContent) {
self.beacon_info = beacon_info;
}
/// An optional human-readable description for this sharing session
/// (from the originating `beacon_info` event).
pub fn description(&self) -> Option<&str> {
self.beacon_info.description.as_deref()
}
/// The full `beacon_info` event content that started this session.
pub fn beacon_info(&self) -> &BeaconInfoEventContent {
&self.beacon_info
}
}

View File

@@ -52,6 +52,7 @@ use ruma::{
room_version_rules::RedactionRules,
};
mod live_location;
mod message;
mod msg_like;
pub(super) mod other;
@@ -65,6 +66,7 @@ pub(in crate::timeline) use self::message::{
extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
};
pub use self::{
live_location::{BeaconInfo, LiveLocationState},
message::Message,
msg_like::{MsgLikeContent, MsgLikeKind, ThreadSummary},
other::OtherMessageLike,
@@ -114,6 +116,13 @@ pub enum TimelineItemContent {
/// An `m.rtc.notification` event
RtcNotification,
/// A live location sharing session (MSC3489).
///
/// Created from an `org.matrix.msc3672.beacon_info` state event.
/// Subsequent `org.matrix.msc3672.beacon` message-like events are
/// aggregated onto this item.
LiveLocation(LiveLocationState),
}
impl TimelineItemContent {
@@ -121,6 +130,26 @@ impl TimelineItemContent {
as_variant!(self, TimelineItemContent::MsgLike)
}
/// If `self` is of the [`LiveLocation`][Self::LiveLocation] variant, return
/// the inner [`LiveLocationState`].
pub fn as_live_location_state(&self) -> Option<&LiveLocationState> {
as_variant!(self, Self::LiveLocation)
}
/// If `self` is of the [`LiveLocation`][Self::LiveLocation] variant, return
/// the inner [`LiveLocationState`] mutably.
pub(in crate::timeline) fn as_live_location_state_mut(
&mut self,
) -> Option<&mut LiveLocationState> {
as_variant!(self, Self::LiveLocation)
}
/// Check whether this item's content is a live location
/// [`LiveLocation`][Self::LiveLocation].
pub fn is_live_location(&self) -> bool {
matches!(self, Self::LiveLocation(_))
}
/// If `self` is of the [`MsgLike`][Self::MsgLike] variant, return the
/// inner [`Message`].
pub fn as_message(&self) -> Option<&Message> {
@@ -227,6 +256,7 @@ impl TimelineItemContent {
| TimelineItemContent::FailedToParseState { .. } => "an event that couldn't be parsed",
TimelineItemContent::CallInvite => "a call invite",
TimelineItemContent::RtcNotification => "a call notification",
TimelineItemContent::LiveLocation(_) => "a live location share",
}
}
@@ -297,7 +327,7 @@ impl TimelineItemContent {
pub(in crate::timeline) fn redact(&self, rules: &RedactionRules) -> Self {
match self {
Self::MsgLike(_) | Self::CallInvite | Self::RtcNotification => {
Self::MsgLike(_) | Self::CallInvite | Self::RtcNotification | Self::LiveLocation(_) => {
TimelineItemContent::MsgLike(MsgLikeContent::redacted())
}
Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(rules)),
@@ -329,7 +359,8 @@ impl TimelineItemContent {
| TimelineItemContent::FailedToParseMessageLike { .. }
| TimelineItemContent::FailedToParseState { .. }
| TimelineItemContent::CallInvite
| TimelineItemContent::RtcNotification => {
| TimelineItemContent::RtcNotification
| TimelineItemContent::LiveLocation(..) => {
// No reactions for these kind of items.
None
}
@@ -354,7 +385,8 @@ impl TimelineItemContent {
| TimelineItemContent::FailedToParseMessageLike { .. }
| TimelineItemContent::FailedToParseState { .. }
| TimelineItemContent::CallInvite
| TimelineItemContent::RtcNotification => {
| TimelineItemContent::RtcNotification
| TimelineItemContent::LiveLocation(..) => {
// No reactions for these kind of items.
None
}

View File

@@ -40,10 +40,10 @@ mod remote;
pub use self::{
content::{
AnyOtherStateEventContentChange, EmbeddedEvent, EncryptedMessage, InReplyToDetails,
MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind,
OtherMessageLike, OtherState, PollResult, PollState, RoomMembershipChange,
RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent,
AnyOtherStateEventContentChange, BeaconInfo, EmbeddedEvent, EncryptedMessage,
InReplyToDetails, LiveLocationState, MemberProfileChange, MembershipChange, Message,
MsgLikeContent, MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState,
RoomMembershipChange, RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent,
},
local::{EventSendState, MediaUploadProgress},
};
@@ -562,7 +562,8 @@ impl EventTimelineItem {
| TimelineItemContent::FailedToParseMessageLike { .. }
| TimelineItemContent::FailedToParseState { .. }
| TimelineItemContent::CallInvite
| TimelineItemContent::RtcNotification => None,
| TimelineItemContent::RtcNotification
| TimelineItemContent::LiveLocation(_) => None,
};
if let Some(body) = body {

View File

@@ -88,12 +88,12 @@ pub use self::{
error::*,
event_filter::{TimelineEventCondition, TimelineEventFilter},
event_item::{
AnyOtherStateEventContentChange, EmbeddedEvent, EncryptedMessage, EventItemOrigin,
EventSendState, EventTimelineItem, InReplyToDetails, MediaUploadProgress,
MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind,
OtherMessageLike, OtherState, PollResult, PollState, Profile, ReactionInfo, ReactionStatus,
ReactionsByKeyBySender, RoomMembershipChange, RoomPinnedEventsChange, Sticker,
ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineEventShieldState,
AnyOtherStateEventContentChange, BeaconInfo, EmbeddedEvent, EncryptedMessage,
EventItemOrigin, EventSendState, EventTimelineItem, InReplyToDetails, LiveLocationState,
MediaUploadProgress, MemberProfileChange, MembershipChange, Message, MsgLikeContent,
MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState, Profile, ReactionInfo,
ReactionStatus, ReactionsByKeyBySender, RoomMembershipChange, RoomPinnedEventsChange,
Sticker, ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineEventShieldState,
TimelineEventShieldStateCode, TimelineItemContent,
},
item::{TimelineItem, TimelineItemKind, TimelineUniqueId},