mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-04-22 16:17:31 -04:00
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:
committed by
Stefan Ceriu
parent
17a9ab41e4
commit
e61e86c3b4
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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},
|
||||
|
||||
Reference in New Issue
Block a user