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}