Skip to main content

matrix_sdk_ui/timeline/event_item/content/
live_location.rs

1// Copyright 2026 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//! Timeline item content for live location sharing (MSC3489).
16//!
17//! Live location sharing uses two event types:
18//! - `org.matrix.msc3672.beacon_info` (state event): starts/stops a sharing
19//!   session and creates the timeline item represented by
20//!   [`LiveLocationState`].
21//! - `org.matrix.msc3672.beacon` (message-like event): periodic location
22//!   updates that are aggregated onto the parent [`LiveLocationState`] item.
23
24use std::sync::Arc;
25
26use matrix_sdk::deserialized_responses::EncryptionInfo;
27use ruma::{
28    MilliSecondsSinceUnixEpoch,
29    events::{beacon_info::BeaconInfoEventContent, location::AssetType},
30};
31
32/// A single location update received from a beacon event.
33///
34/// Created from an `org.matrix.msc3672.beacon` message-like event and
35/// aggregated onto the parent [`LiveLocationState`] timeline item.
36#[derive(Clone, Debug)]
37pub struct BeaconInfo {
38    /// The geo URI carrying the user's coordinates (e.g.
39    /// `"geo:51.5008,0.1247;u=35"`).
40    pub(in crate::timeline) geo_uri: String,
41
42    /// Timestamp of this location update (from the beacon event's
43    /// `org.matrix.msc3488.ts` field).
44    pub(in crate::timeline) ts: MilliSecondsSinceUnixEpoch,
45
46    /// An optional human-readable description of the location.
47    pub(in crate::timeline) description: Option<String>,
48
49    /// Encryption info of the beacon event that carried this location update.
50    ///
51    /// `Some` when the beacon event was encrypted and successfully decrypted,
52    /// `None` when it was sent in the clear (or when encryption info is
53    /// unavailable).
54    pub(in crate::timeline) encryption_info: Option<Arc<EncryptionInfo>>,
55}
56
57impl BeaconInfo {
58    /// The geo URI of this location update.
59    pub fn geo_uri(&self) -> &str {
60        &self.geo_uri
61    }
62
63    /// The timestamp of this location update.
64    pub fn ts(&self) -> MilliSecondsSinceUnixEpoch {
65        self.ts
66    }
67
68    /// An optional human-readable description of this location.
69    pub fn description(&self) -> Option<&str> {
70        self.description.as_deref()
71    }
72
73    /// The encryption info of the beacon event that carried this location
74    /// update, if it was encrypted and successfully decrypted.
75    pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
76        self.encryption_info.as_deref()
77    }
78}
79
80/// The state of a live location sharing session.
81///
82/// Created when a `org.matrix.msc3672.beacon_info` state event is received.
83/// Subsequent `org.matrix.msc3672.beacon` message-like events are aggregated
84/// onto this item, appending to [`LiveLocationState::locations`].
85///
86/// When a user stops sharing (a new `beacon_info` with `live: false` arrives)
87/// a *separate* timeline item is created for the stop event. The original
88/// item's liveness can be checked via [`LiveLocationState::is_live`], which
89/// internally checks both the `live` flag and the session timeout.
90#[derive(Clone, Debug)]
91pub struct LiveLocationState {
92    /// The content of the `beacon_info` state event that created this item.
93    pub(in crate::timeline) beacon_info: BeaconInfoEventContent,
94
95    /// All location updates aggregated onto this session, kept sorted by
96    /// timestamp.
97    pub(in crate::timeline) locations: Vec<BeaconInfo>,
98}
99
100impl LiveLocationState {
101    /// Create a new [`LiveLocationState`] from the given
102    /// [`BeaconInfoEventContent`].
103    pub fn new(beacon_info: BeaconInfoEventContent) -> Self {
104        Self { beacon_info, locations: Vec::new() }
105    }
106
107    /// Add a location update. Keeps the internal list sorted by timestamp so
108    /// that [`LiveLocationState::latest_location`] always returns the most
109    /// recent one.
110    pub(in crate::timeline) fn add_location(&mut self, location: BeaconInfo) {
111        match self.locations.binary_search_by_key(&location.ts, |l| l.ts) {
112            Ok(_) => (), // Duplicate timestamp, do nothing.
113            Err(index) => self.locations.insert(index, location),
114        }
115    }
116
117    /// Remove the location update with the given timestamp. Used when
118    /// unapplying an aggregation (e.g. event cache moves an event).
119    pub(in crate::timeline) fn remove_location(&mut self, ts: MilliSecondsSinceUnixEpoch) {
120        self.locations.retain(|l| l.ts != ts);
121    }
122
123    /// All accumulated location updates, sorted by timestamp (oldest first).
124    pub fn locations(&self) -> &[BeaconInfo] {
125        &self.locations
126    }
127
128    /// The most recent location update, if any have been received.
129    pub fn latest_location(&self) -> Option<&BeaconInfo> {
130        self.locations.last()
131    }
132
133    /// Whether this live location share is still active.
134    ///
135    /// Returns `false` once the `live` flag has been set to `false` **or**
136    /// the session's timeout has elapsed.
137    pub fn is_live(&self) -> bool {
138        self.beacon_info.is_live()
139    }
140
141    /// The timestamp when this live location sharing session started
142    /// (from the `org.matrix.msc3488.ts` field of the originating
143    /// `beacon_info` state event).
144    ///
145    /// This marks the *beginning* of the session. The session expires at
146    /// `ts + timeout` — see [`LiveLocationState::is_live`] and
147    /// [`LiveLocationState::timeout`].
148    pub fn ts(&self) -> MilliSecondsSinceUnixEpoch {
149        self.beacon_info.ts
150    }
151
152    /// An optional human-readable description for this sharing session
153    /// (from the originating `beacon_info` event).
154    pub fn description(&self) -> Option<&str> {
155        self.beacon_info.description.as_deref()
156    }
157
158    /// The duration that the location sharing will be live.
159    ///
160    /// Meaning that the location will stop being shared at `ts + timeout`.
161    pub fn timeout(&self) -> std::time::Duration {
162        self.beacon_info.timeout
163    }
164
165    /// The asset type of the beacon (e.g. `Sender` for the user's own
166    /// location, `Pin` for a fixed point of interest).
167    pub fn asset_type(&self) -> AssetType {
168        self.beacon_info.asset.type_.clone()
169    }
170
171    /// Update this session with a stop `beacon_info` event (one where
172    /// `live` is `false`). This replaces the stored content so that
173    /// [`LiveLocationState::is_live`] will return `false`.
174    pub(in crate::timeline) fn stop(&mut self, beacon_info: BeaconInfoEventContent) {
175        assert!(!beacon_info.is_live(), "A stop `beacon_info` event must not be live.");
176        self.beacon_info = beacon_info;
177    }
178
179    /// Check if a stop `beacon_info` matches this session.
180    ///
181    /// Returns `true` if all fields except `live` match and this session is
182    /// still live. This is used to verify that a stop event belongs to the
183    /// same session as this start event.
184    pub(in crate::timeline) fn matches_stop(&self, stop: &BeaconInfoEventContent) -> bool {
185        self.beacon_info.live && beacon_info_matches(&self.beacon_info, stop)
186    }
187}
188
189/// Check if two `BeaconInfoEventContent` values belong to the same session.
190///
191/// Compares all fields except `live`, which differs between start and stop
192/// events. Returns `true` if all other fields match.
193pub fn beacon_info_matches(a: &BeaconInfoEventContent, b: &BeaconInfoEventContent) -> bool {
194    a.ts == b.ts && a.timeout == b.timeout && a.description == b.description && a.asset == b.asset
195}