-
Notifications
You must be signed in to change notification settings - Fork 260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
sdk: implement MSC3489 live location sharing #3621
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -160,6 +160,20 @@ impl Room { | |
} | ||
} | ||
|
||
pub async fn send_beacon_info(&self, duration_millis: u64) -> Result<(), ClientError> { | ||
RUNTIME.block_on(async move { | ||
self.inner.send_beacon_info(duration_millis).await?; | ||
Ok(()) | ||
}) | ||
} | ||
|
||
pub async fn stop_beacon_info(&self) -> Result<(), ClientError> { | ||
RUNTIME.block_on(async move { | ||
self.inner.stop_beacon_info().await?; | ||
Ok(()) | ||
}) | ||
Comment on lines
+164
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In both cases: no need for |
||
} | ||
|
||
/// Forces the currently active room key, which is used to encrypt messages, | ||
/// to be rotated. | ||
/// | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,7 @@ use ruma::events::{ | |
use tracing::warn; | ||
|
||
use super::ProfileDetails; | ||
use crate::ruma::{ImageInfo, MessageType, PollKind}; | ||
use crate::ruma::{ImageInfo, LocationContent, MessageType, PollKind}; | ||
|
||
#[derive(Clone, uniffi::Object)] | ||
pub struct TimelineItemContent(pub(crate) matrix_sdk_ui::timeline::TimelineItemContent); | ||
|
@@ -44,6 +44,29 @@ impl TimelineItemContent { | |
source: Arc::new(MediaSource::from(content.source.clone())), | ||
} | ||
} | ||
Content::BeaconInfoState(beacon_state) => { | ||
let Some(location) = beacon_state.last_location() else { | ||
return TimelineItemContentKind::FailedToParseMessageLike { | ||
event_type: "org.matrix.msc3672.beacon".to_string(), | ||
error: "Could not find beacon last location content".to_string(), | ||
}; | ||
}; | ||
Comment on lines
+48
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the last location was not set, then I think the TimelineItemContent should not exist in the first place. |
||
|
||
let body = location.description.unwrap_or_else(|| "Location".to_string()); | ||
|
||
let location = LocationContent { | ||
body, | ||
geo_uri: location.uri, | ||
description: None, | ||
zoom_level: None, | ||
asset: None, | ||
}; | ||
|
||
TimelineItemContentKind::BeaconInfoState { | ||
location, | ||
user_id: String::from(beacon_state.user_id()), | ||
} | ||
} | ||
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()), | ||
Content::CallInvite => TimelineItemContentKind::CallInvite, | ||
Content::CallNotify => TimelineItemContentKind::CallNotify, | ||
|
@@ -110,6 +133,10 @@ impl TimelineItemContent { | |
|
||
#[derive(uniffi::Enum)] | ||
pub enum TimelineItemContentKind { | ||
BeaconInfoState { | ||
location: LocationContent, | ||
user_id: String, | ||
}, | ||
Message, | ||
RedactedMessage, | ||
Sticker { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,16 +19,21 @@ use as_variant::as_variant; | |
use content::{InReplyToDetails, RepliedToEventDetails}; | ||
use eyeball_im::VectorDiff; | ||
use futures_util::{pin_mut, StreamExt as _}; | ||
use matrix_sdk::attachment::{ | ||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, | ||
BaseThumbnailInfo, BaseVideoInfo, Thumbnail, | ||
use matrix_sdk::{ | ||
attachment::{ | ||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, | ||
BaseThumbnailInfo, BaseVideoInfo, Thumbnail, | ||
}, | ||
deserialized_responses::SyncOrStrippedState, | ||
}; | ||
use matrix_sdk_ui::timeline::{ | ||
EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails, | ||
}; | ||
use mime::Mime; | ||
use ruma::{ | ||
events::{ | ||
beacon::BeaconEventContent, | ||
beacon_info::BeaconInfoEventContent, | ||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel}, | ||
poll::{ | ||
unstable_end::UnstablePollEndEventContent, | ||
|
@@ -44,15 +49,15 @@ use ruma::{ | |
ForwardThread, LocationMessageEventContent, MessageType, | ||
RoomMessageEventContentWithoutRelation, | ||
}, | ||
AnyMessageLikeEventContent, | ||
AnyMessageLikeEventContent, SyncStateEvent, | ||
}, | ||
EventId, OwnedTransactionId, | ||
}; | ||
use tokio::{ | ||
sync::Mutex, | ||
task::{AbortHandle, JoinHandle}, | ||
}; | ||
use tracing::{error, warn}; | ||
use tracing::{error, info, warn}; | ||
use uuid::Uuid; | ||
|
||
use self::content::{Reaction, ReactionSenderData, TimelineItemContent}; | ||
|
@@ -521,6 +526,51 @@ impl Timeline { | |
Ok(()) | ||
} | ||
|
||
/// Sends a user's location as a beacon based on their last beacon_info | ||
/// event. | ||
/// | ||
/// Retrieves the last beacon_info from the room state and sends a beacon | ||
/// with the geo_uri. Since only one active beacon_info per user per | ||
/// room is allowed, we can grab the currently live beacon_info event | ||
/// from the room state. | ||
/// | ||
/// TODO: Does the logic belong in self.inner.room() or here? | ||
pub async fn send_user_location_beacon( | ||
self: Arc<Self>, | ||
geo_uri: String, | ||
) -> Result<(), ClientError> { | ||
Comment on lines
+538
to
+541
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's way too much logic in this function; it should not live at the FFI layer, otherwise it's ~impossible to test. |
||
let Some(raw_event) = self | ||
.inner | ||
.room() | ||
.get_state_event_static_for_key::<BeaconInfoEventContent, _>( | ||
self.inner.room().own_user_id(), | ||
) | ||
.await? | ||
else { | ||
todo!("How to handle case of missing beacon event for state key?") | ||
}; | ||
|
||
match raw_event.deserialize() { | ||
Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(beacon_info))) => { | ||
torrybr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if beacon_info.content.is_live() { | ||
let beacon_event = | ||
BeaconEventContent::new(beacon_info.event_id, geo_uri.clone(), None); | ||
let message_content = AnyMessageLikeEventContent::Beacon(beacon_event.clone()); | ||
|
||
RUNTIME.spawn(async move { | ||
self.inner.send(message_content).await; | ||
}); | ||
} | ||
} | ||
Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => {} | ||
Ok(SyncOrStrippedState::Stripped(_)) => {} | ||
Err(e) => { | ||
info!(room_id = ?self.inner.room().room_id(), "Could not deserialize m.beacon_info: {e}"); | ||
} | ||
} | ||
Ok(()) | ||
} | ||
|
||
pub async fn send_location( | ||
self: Arc<Self>, | ||
body: String, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
//! This module handles rendering of MSC3489 live location sharing in the | ||
//! timeline. | ||
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think having this in the timeline is the proper cutting point. Instead, this would be more useful to have at the SDK layer, and maybe even with a slightly different API. |
||
|
||
use std::collections::HashMap; | ||
|
||
use ruma::{ | ||
events::{ | ||
beacon::BeaconEventContent, beacon_info::BeaconInfoEventContent, location::LocationContent, | ||
FullStateEventContent, | ||
}, | ||
EventId, OwnedEventId, OwnedUserId, | ||
}; | ||
|
||
/// Holds the state of a beacon_info. | ||
/// | ||
/// This struct should be created for each beacon_info event handled and then | ||
/// updated whenever handling any beacon event that relates to the same | ||
/// beacon_info event. | ||
#[derive(Clone, Debug)] | ||
pub struct BeaconState { | ||
pub(super) beacon_info_event_content: BeaconInfoEventContent, | ||
pub(super) last_location: Option<LocationContent>, | ||
pub(super) user_id: OwnedUserId, | ||
} | ||
|
||
impl BeaconState { | ||
pub(super) fn new( | ||
content: FullStateEventContent<BeaconInfoEventContent>, | ||
user_id: OwnedUserId, | ||
) -> Self { | ||
match &content { | ||
FullStateEventContent::Original { content, .. } => BeaconState { | ||
beacon_info_event_content: content.clone(), | ||
last_location: None, | ||
user_id, | ||
}, | ||
FullStateEventContent::Redacted(_) => { | ||
todo!("How should this be handled?") | ||
torrybr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
|
||
/// Update the state with the last known associated beacon info. | ||
/// | ||
/// Used when a new beacon_info event is sent with the live field set | ||
/// to false. | ||
pub(super) fn update_beacon_info(&self, content: &BeaconInfoEventContent) -> Self { | ||
let mut clone = self.clone(); | ||
clone.beacon_info_event_content = content.clone(); | ||
clone | ||
} | ||
|
||
/// Update the state with the last known associated beacon location. | ||
pub(super) fn update_beacon(&self, content: &BeaconEventContent) -> Self { | ||
let mut clone = self.clone(); | ||
clone.last_location = Some(content.location.clone()); | ||
clone | ||
} | ||
|
||
/// Get the last known beacon location. | ||
pub fn last_location(&self) -> Option<LocationContent> { | ||
self.last_location.clone() | ||
} | ||
|
||
/// Get the user_id of the user who sent the beacon_info event. | ||
pub fn user_id(&self) -> OwnedUserId { | ||
self.user_id.clone() | ||
} | ||
} | ||
|
||
impl From<BeaconState> for BeaconInfoEventContent { | ||
fn from(value: BeaconState) -> Self { | ||
BeaconInfoEventContent::new( | ||
value.beacon_info_event_content.description.clone(), | ||
value.beacon_info_event_content.timeout, | ||
value.beacon_info_event_content.live, | ||
None, | ||
) | ||
} | ||
} | ||
|
||
/// Acts as a cache for beacons before their beacon_infos have been handled. | ||
#[derive(Clone, Debug, Default)] | ||
pub(super) struct BeaconPendingEvents { | ||
pub(super) pending_beacons: HashMap<OwnedEventId, BeaconEventContent>, | ||
} | ||
|
||
impl BeaconPendingEvents { | ||
pub(super) fn add_beacon(&mut self, start_id: &EventId, content: &BeaconEventContent) { | ||
self.pending_beacons.insert(start_id.to_owned(), content.clone()); | ||
} | ||
pub(super) fn apply(&mut self, beacon_info_event_id: &EventId, beacon_state: &mut BeaconState) { | ||
if let Some(newest_beacon) = self.pending_beacons.get_mut(beacon_info_event_id) { | ||
beacon_state.last_location = Some(newest_beacon.location.clone()); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this contain the location too?