change(live_location): Move LiveLocation out of TimelineItemContent and into MsgLikeKind
... so that it has access to `MsgLikeContent` reactions
This commit is contained in:
committed by
Stefan Ceriu
parent
538afaf252
commit
290650c6b1
@@ -36,6 +36,9 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Features
|
||||
|
||||
- [**breaking**] Move `LiveLocation` out of `TimelineItemContent` and into `MsgLikeKind`
|
||||
so it has access to `MsgLikeContent` `reactions`.
|
||||
([#6286](https://github.com/matrix-org/matrix-rust-sdk/pull/6286))
|
||||
- Add `HumanQrLoginError::UnsupportedQrCodeType` for when a QR is parseable but cannot be used to
|
||||
complete a login.
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6285)
|
||||
|
||||
@@ -96,28 +96,6 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
error: error.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
Content::LiveLocation(state) => {
|
||||
let locations = state
|
||||
.locations()
|
||||
.iter()
|
||||
.map(|location| BeaconInfo {
|
||||
geo_uri: location.geo_uri().to_owned(),
|
||||
ts: location.ts().into(),
|
||||
description: location.description().map(ToOwned::to_owned),
|
||||
})
|
||||
.collect();
|
||||
|
||||
TimelineItemContent::LiveLocation {
|
||||
content: LiveLocationContent {
|
||||
is_live: state.is_live(),
|
||||
description: state.description().map(ToOwned::to_owned),
|
||||
timeout_ms: state.timeout().as_millis() as u64,
|
||||
asset_type: state.asset_type().into(),
|
||||
locations,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,13 +185,6 @@ pub enum TimelineItemContent {
|
||||
state_key: String,
|
||||
error: String,
|
||||
},
|
||||
/// A live location sharing session (MSC3489).
|
||||
///
|
||||
/// Represents a `org.matrix.msc3672.beacon_info` state event with all
|
||||
/// aggregated location updates from `org.matrix.msc3672.beacon` events.
|
||||
LiveLocation {
|
||||
content: LiveLocationContent,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
|
||||
@@ -18,7 +18,7 @@ use matrix_sdk_base::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
|
||||
|
||||
use super::{
|
||||
content::Reaction,
|
||||
content::{BeaconInfo, LiveLocationContent, Reaction},
|
||||
reply::{EmbeddedEventDetails, InReplyToDetails},
|
||||
};
|
||||
use crate::{
|
||||
@@ -54,6 +54,12 @@ pub enum MsgLikeKind {
|
||||
|
||||
/// A custom message like event.
|
||||
Other { event_type: MessageLikeEventType },
|
||||
|
||||
/// A live location sharing session (MSC3489).
|
||||
///
|
||||
/// Represents a `org.matrix.msc3672.beacon_info` state event with all
|
||||
/// aggregated location updates from `org.matrix.msc3672.beacon` events.
|
||||
LiveLocation { content: LiveLocationContent },
|
||||
}
|
||||
|
||||
/// A special kind of [`super::TimelineItemContent`] that groups together
|
||||
@@ -195,6 +201,33 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
thread_root,
|
||||
thread_summary,
|
||||
},
|
||||
Kind::LiveLocation(state) => {
|
||||
let locations = state
|
||||
.locations()
|
||||
.iter()
|
||||
.map(|location| BeaconInfo {
|
||||
geo_uri: location.geo_uri().to_owned(),
|
||||
ts: location.ts().into(),
|
||||
description: location.description().map(ToOwned::to_owned),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
kind: MsgLikeKind::LiveLocation {
|
||||
content: LiveLocationContent {
|
||||
is_live: state.is_live(),
|
||||
description: state.description().map(ToOwned::to_owned),
|
||||
timeout_ms: state.timeout().as_millis() as u64,
|
||||
asset_type: state.asset_type().into(),
|
||||
locations,
|
||||
},
|
||||
},
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] Move `LiveLocation` out of `TimelineItemContent` and into `MsgLikeKind`
|
||||
so it has access to `MsgLikeContent` `reactions`.
|
||||
([#6286](https://github.com/matrix-org/matrix-rust-sdk/pull/6286))
|
||||
- [**breaking**] Rename `AnyOtherFullStateEventContent` to `AnyOtherStateEventContentChange`
|
||||
to match the name change in the upstream types.
|
||||
([#6218](https://github.com/matrix-org/matrix-rust-sdk/pull/6218))
|
||||
|
||||
@@ -322,9 +322,13 @@ impl TimelineAction {
|
||||
AnySyncStateEvent::BeaconInfo(ev) => match ev {
|
||||
SyncStateEvent::Original(ev) => {
|
||||
if ev.content.is_live() {
|
||||
Self::add_item(TimelineItemContent::LiveLocation(
|
||||
LiveLocationState::new(ev.content),
|
||||
))
|
||||
Self::add_item(TimelineItemContent::MsgLike(MsgLikeContent {
|
||||
kind: MsgLikeKind::LiveLocation(LiveLocationState::new(ev.content)),
|
||||
reactions: Default::default(),
|
||||
thread_root: None,
|
||||
in_reply_to: None,
|
||||
thread_summary: None,
|
||||
}))
|
||||
} else {
|
||||
// A non-live beacon_info is a stop event: it should update the
|
||||
// existing live item from the same sender rather than creating a
|
||||
|
||||
@@ -116,13 +116,6 @@ 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 {
|
||||
@@ -130,24 +123,32 @@ impl TimelineItemContent {
|
||||
as_variant!(self, TimelineItemContent::MsgLike)
|
||||
}
|
||||
|
||||
/// If `self` is of the [`LiveLocation`][Self::LiveLocation] variant, return
|
||||
/// the inner [`LiveLocationState`].
|
||||
/// If `self` is of the [`MsgLike`][Self::MsgLike] variant with a
|
||||
/// [`LiveLocation`][MsgLikeKind::LiveLocation] kind, return the inner
|
||||
/// [`LiveLocationState`].
|
||||
pub fn as_live_location_state(&self) -> Option<&LiveLocationState> {
|
||||
as_variant!(self, Self::LiveLocation)
|
||||
as_variant!(self, Self::MsgLike(MsgLikeContent {
|
||||
kind: MsgLikeKind::LiveLocation(state),
|
||||
..
|
||||
}) => state)
|
||||
}
|
||||
|
||||
/// If `self` is of the [`LiveLocation`][Self::LiveLocation] variant, return
|
||||
/// the inner [`LiveLocationState`] mutably.
|
||||
/// If `self` is of the [`MsgLike`][Self::MsgLike] variant with a
|
||||
/// [`LiveLocation`][MsgLikeKind::LiveLocation] kind, 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)
|
||||
as_variant!(self, Self::MsgLike(MsgLikeContent {
|
||||
kind: MsgLikeKind::LiveLocation(state),
|
||||
..
|
||||
}) => state)
|
||||
}
|
||||
|
||||
/// Check whether this item's content is a live location
|
||||
/// [`LiveLocation`][Self::LiveLocation].
|
||||
/// Check whether this item's content is a
|
||||
/// [`LiveLocation`][MsgLikeKind::LiveLocation].
|
||||
pub fn is_live_location(&self) -> bool {
|
||||
matches!(self, Self::LiveLocation(_))
|
||||
matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::LiveLocation(_), .. }))
|
||||
}
|
||||
|
||||
/// If `self` is of the [`MsgLike`][Self::MsgLike] variant, return the
|
||||
@@ -256,7 +257,6 @@ 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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ impl TimelineItemContent {
|
||||
|
||||
pub(in crate::timeline) fn redact(&self, rules: &RedactionRules) -> Self {
|
||||
match self {
|
||||
Self::MsgLike(_) | Self::CallInvite | Self::RtcNotification | Self::LiveLocation(_) => {
|
||||
Self::MsgLike(_) | Self::CallInvite | Self::RtcNotification => {
|
||||
TimelineItemContent::MsgLike(MsgLikeContent::redacted())
|
||||
}
|
||||
Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(rules)),
|
||||
@@ -359,8 +359,7 @@ impl TimelineItemContent {
|
||||
| TimelineItemContent::FailedToParseMessageLike { .. }
|
||||
| TimelineItemContent::FailedToParseState { .. }
|
||||
| TimelineItemContent::CallInvite
|
||||
| TimelineItemContent::RtcNotification
|
||||
| TimelineItemContent::LiveLocation(..) => {
|
||||
| TimelineItemContent::RtcNotification => {
|
||||
// No reactions for these kind of items.
|
||||
None
|
||||
}
|
||||
@@ -385,8 +384,7 @@ impl TimelineItemContent {
|
||||
| TimelineItemContent::FailedToParseMessageLike { .. }
|
||||
| TimelineItemContent::FailedToParseState { .. }
|
||||
| TimelineItemContent::CallInvite
|
||||
| TimelineItemContent::RtcNotification
|
||||
| TimelineItemContent::LiveLocation(..) => {
|
||||
| TimelineItemContent::RtcNotification => {
|
||||
// No reactions for these kind of items.
|
||||
None
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
use as_variant::as_variant;
|
||||
use ruma::OwnedEventId;
|
||||
|
||||
use super::{EmbeddedEvent, EncryptedMessage, InReplyToDetails, Message, PollState, Sticker};
|
||||
use super::{
|
||||
EmbeddedEvent, EncryptedMessage, InReplyToDetails, LiveLocationState, Message, PollState,
|
||||
Sticker,
|
||||
};
|
||||
use crate::timeline::{
|
||||
ReactionsByKeyBySender, TimelineDetails, event_item::content::other::OtherMessageLike,
|
||||
};
|
||||
@@ -39,6 +42,9 @@ pub enum MsgLikeKind {
|
||||
|
||||
/// A custom message like event.
|
||||
Other(OtherMessageLike),
|
||||
|
||||
/// A live location sharing session (MSC3489).
|
||||
LiveLocation(LiveLocationState),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -87,6 +93,7 @@ impl MsgLikeContent {
|
||||
MsgLikeKind::Redacted => "a redacted message",
|
||||
MsgLikeKind::UnableToDecrypt(_) => "an encrypted message we couldn't decrypt",
|
||||
MsgLikeKind::Other(_) => "a custom message-like event",
|
||||
MsgLikeKind::LiveLocation(_) => "a live location share",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -554,7 +554,8 @@ impl EventTimelineItem {
|
||||
| MsgLikeKind::Poll(_)
|
||||
| MsgLikeKind::Redacted
|
||||
| MsgLikeKind::UnableToDecrypt(_)
|
||||
| MsgLikeKind::Other(_) => None,
|
||||
| MsgLikeKind::Other(_)
|
||||
| MsgLikeKind::LiveLocation(_) => None,
|
||||
},
|
||||
TimelineItemContent::MembershipChange(_)
|
||||
| TimelineItemContent::ProfileChange(_)
|
||||
@@ -562,8 +563,7 @@ impl EventTimelineItem {
|
||||
| TimelineItemContent::FailedToParseMessageLike { .. }
|
||||
| TimelineItemContent::FailedToParseState { .. }
|
||||
| TimelineItemContent::CallInvite
|
||||
| TimelineItemContent::RtcNotification
|
||||
| TimelineItemContent::LiveLocation(_) => None,
|
||||
| TimelineItemContent::RtcNotification => None,
|
||||
};
|
||||
|
||||
if let Some(body) = body {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use assert_matches2::assert_matches;
|
||||
use eyeball_im::VectorDiff;
|
||||
use matrix_sdk_test::{ALICE, BOB, async_test};
|
||||
use ruma::{
|
||||
@@ -24,9 +25,11 @@ use ruma::{
|
||||
};
|
||||
use stream_assert::{assert_next_matches, assert_pending};
|
||||
|
||||
use crate::timeline::{EventTimelineItem, tests::TestTimeline};
|
||||
use crate::timeline::{
|
||||
EventTimelineItem, ReactionStatus, TimelineEventItemId, tests::TestTimeline,
|
||||
};
|
||||
|
||||
/// A `beacon_info` state event creates a `TimelineItemContent::LiveLocation`
|
||||
/// A `beacon_info` state event creates a `MsgLikeKind::LiveLocation`
|
||||
/// item with `is_live() == true` and no accumulated locations.
|
||||
#[async_test]
|
||||
async fn test_beacon_info_creates_timeline_item() {
|
||||
@@ -419,6 +422,131 @@ async fn test_redacted_beacon_info_produces_redacted_item() {
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
/// A reaction on a live location item is aggregated onto the item and exposed
|
||||
/// via `reactions()`.
|
||||
#[async_test]
|
||||
async fn test_reaction_on_live_location_item() {
|
||||
let timeline = TestTimeline::new();
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let beacon_id = event_id!("$beacon_info:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
assert!(item.content().as_live_location_state().is_some());
|
||||
assert!(item.content().reactions().unwrap().is_empty());
|
||||
|
||||
// BOB reacts to the live location item.
|
||||
timeline.handle_live_event(timeline.factory.reaction(beacon_id, "👍").sender(&BOB)).await;
|
||||
|
||||
// The item is updated in-place with the reaction applied.
|
||||
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
|
||||
assert!(item.content().as_live_location_state().is_some(), "still a live location item");
|
||||
|
||||
let reactions = item.content().reactions().expect("live location should expose reactions");
|
||||
let thumbs_up = reactions.get("👍").expect("👍 reaction should be present");
|
||||
let reaction = thumbs_up.get(*BOB).expect("BOB's reaction should be present");
|
||||
assert_matches!(&reaction.status, ReactionStatus::RemoteToRemote(_));
|
||||
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
/// Multiple reactions from different senders are all aggregated onto a live
|
||||
/// location item.
|
||||
#[async_test]
|
||||
async fn test_multiple_reactions_on_live_location_item() {
|
||||
let timeline = TestTimeline::new();
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let beacon_id = event_id!("$beacon_info:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
let _item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
|
||||
// ALICE and BOB both react, with different keys.
|
||||
timeline.handle_live_event(timeline.factory.reaction(beacon_id, "👍").sender(&ALICE)).await;
|
||||
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
|
||||
let reactions = item.content().reactions().unwrap();
|
||||
assert_eq!(reactions.len(), 1);
|
||||
assert!(reactions.get("👍").unwrap().get(*ALICE).is_some());
|
||||
|
||||
timeline.handle_live_event(timeline.factory.reaction(beacon_id, "❤️").sender(&BOB)).await;
|
||||
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
|
||||
|
||||
let reactions = item.content().reactions().unwrap();
|
||||
assert_eq!(reactions.len(), 2, "two distinct reaction keys");
|
||||
assert!(reactions.get("👍").unwrap().get(*ALICE).is_some());
|
||||
assert!(reactions.get("❤️").unwrap().get(*BOB).is_some());
|
||||
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
/// A reaction that arrives *before* its target live location item is stashed
|
||||
/// and applied once the beacon_info item is inserted.
|
||||
#[async_test]
|
||||
async fn test_reaction_before_live_location_item_is_applied_when_parent_arrives() {
|
||||
let timeline = TestTimeline::new();
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let beacon_id = event_id!("$beacon_info:example.org");
|
||||
|
||||
// Reaction arrives before the beacon_info.
|
||||
timeline.handle_live_event(timeline.factory.reaction(beacon_id, "👍").sender(&BOB)).await;
|
||||
|
||||
// Nothing visible yet — no parent item to attach to.
|
||||
assert_pending!(stream);
|
||||
|
||||
// Now the beacon_info arrives.
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
|
||||
// The item is inserted with the reaction already applied.
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
assert!(item.content().as_live_location_state().is_some());
|
||||
|
||||
let reactions = item.content().reactions().expect("live location should expose reactions");
|
||||
let thumbs_up = reactions.get("👍").expect("👍 reaction should be present");
|
||||
assert!(thumbs_up.get(*BOB).is_some(), "BOB's reaction should be pre-applied");
|
||||
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
/// A locally-toggled reaction on a live location item produces a local echo
|
||||
/// and is then confirmed by the remote echo from sync.
|
||||
#[async_test]
|
||||
async fn test_local_reaction_on_live_location_item() {
|
||||
let timeline = TestTimeline::new();
|
||||
let mut stream = timeline.subscribe_events().await;
|
||||
let beacon_id = event_id!("$beacon_info:example.org");
|
||||
|
||||
timeline.send_beacon_info(&ALICE, beacon_id, None, Duration::from_secs(3600), true).await;
|
||||
|
||||
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
|
||||
let item_id = TimelineEventItemId::EventId(item.event_id().unwrap().to_owned());
|
||||
|
||||
// Toggle a reaction locally.
|
||||
timeline.toggle_reaction_local(&item_id, "👍").await.unwrap();
|
||||
|
||||
// The item is updated with a local-echo reaction.
|
||||
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
|
||||
assert!(item.content().as_live_location_state().is_some());
|
||||
let reactions = item.content().reactions().unwrap();
|
||||
let reaction = reactions.get("👍").unwrap().get(*ALICE).unwrap();
|
||||
assert_matches!(
|
||||
&reaction.status,
|
||||
(ReactionStatus::LocalToLocal(_) | ReactionStatus::LocalToRemote(_))
|
||||
);
|
||||
|
||||
// Receive the remote echo from sync.
|
||||
timeline.handle_live_event(timeline.factory.reaction(beacon_id, "👍").sender(&ALICE)).await;
|
||||
|
||||
// The item is updated once more — now the reaction is a confirmed remote echo.
|
||||
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
|
||||
assert!(item.content().as_live_location_state().is_some());
|
||||
let reactions = item.content().reactions().unwrap();
|
||||
let reaction = reactions.get("👍").unwrap().get(*ALICE).unwrap();
|
||||
assert_matches!(&reaction.status, ReactionStatus::RemoteToRemote(_));
|
||||
|
||||
assert_pending!(stream);
|
||||
}
|
||||
|
||||
impl TestTimeline {
|
||||
/// Collect every event timeline item (no virtual items).
|
||||
async fn live_location_event_items(&self) -> Vec<EventTimelineItem> {
|
||||
|
||||
@@ -143,19 +143,20 @@ fn format_timeline_item(item: &Arc<TimelineItem>, is_thread: bool) -> Option<Lis
|
||||
return None;
|
||||
}
|
||||
|
||||
TimelineItemContent::LiveLocation(location) => {
|
||||
match (location.description(), location.latest_location()) {
|
||||
(Some(desc), Some(loc)) => {
|
||||
format!("{sender}: Live location share: {desc} ({})", loc.geo_uri())
|
||||
}
|
||||
(Some(desc), None) => format!("{sender}: Live location share: {desc}"),
|
||||
(None, Some(loc)) => {
|
||||
format!("{sender}: Live location share: {}", loc.geo_uri())
|
||||
}
|
||||
(None, None) => format!("{sender}: Live location share"),
|
||||
TimelineItemContent::MsgLike(MsgLikeContent {
|
||||
kind: MsgLikeKind::LiveLocation(location),
|
||||
..
|
||||
}) => match (location.description(), location.latest_location()) {
|
||||
(Some(desc), Some(loc)) => {
|
||||
format!("{sender}: Live location share: {desc} ({})", loc.geo_uri())
|
||||
}
|
||||
.into()
|
||||
(Some(desc), None) => format!("{sender}: Live location share: {desc}"),
|
||||
(None, Some(loc)) => {
|
||||
format!("{sender}: Live location share: {}", loc.geo_uri())
|
||||
}
|
||||
(None, None) => format!("{sender}: Live location share"),
|
||||
}
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user