Merge branch 'main' into ganfra/kotlin_binding_scripts

This commit is contained in:
ganfra
2023-02-02 17:10:34 +01:00
9 changed files with 410 additions and 28 deletions

View File

@@ -13,7 +13,7 @@ use matrix_sdk::ruma::{
v4::RoomSubscription as RumaRoomSubscription,
UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
assign, IdParseError, OwnedRoomId, UInt,
assign, IdParseError, OwnedRoomId, RoomId, UInt,
};
pub use matrix_sdk::{
room::timeline::Timeline, ruma::api::client::sync::sync_events::v4::SyncRequestListFilters,
@@ -642,7 +642,8 @@ impl SlidingSync {
pub fn get_room(&self, room_id: String) -> anyhow::Result<Option<Arc<SlidingSyncRoom>>> {
let runner = self.inner.clone();
Ok(self.inner.get_room(OwnedRoomId::try_from(room_id)?).map(|inner| {
Ok(self.inner.get_room(<&RoomId>::try_from(room_id.as_str())?).map(|inner| {
Arc::new(SlidingSyncRoom {
inner,
runner,

View File

@@ -463,7 +463,7 @@ impl Message {
// This event ID string will be replaced by something more useful later.
pub fn in_reply_to(&self) -> Option<String> {
self.0.in_reply_to().map(ToString::to_string)
self.0.in_reply_to().map(|r| r.event_id.to_string())
}
pub fn is_edited(&self) -> bool {

View File

@@ -241,6 +241,11 @@ pub enum Error {
#[error(transparent)]
SlidingSync(#[from] crate::sliding_sync::Error),
/// An error occurred in the timeline.
#[cfg(feature = "experimental-timeline")]
#[error(transparent)]
Timeline(#[from] crate::room::timeline::Error),
/// The client is in inconsistent state. This happens when we set a room to
/// a specific type, but then cannot get it in this type.
#[error("The internal client state is inconsistent.")]

View File

@@ -46,8 +46,9 @@ use super::{
MemberProfileChange, OtherState, Profile, RemoteEventTimelineItem, RoomMembershipChange,
Sticker,
},
find_read_marker, rfind_event_by_id, rfind_event_item, EventTimelineItem, Message,
ReactionGroup, TimelineInnerMetadata, TimelineItem, TimelineItemContent, VirtualTimelineItem,
find_read_marker, rfind_event_by_id, rfind_event_item, EventTimelineItem, InReplyToDetails,
Message, ReactionGroup, TimelineInnerMetadata, TimelineItem, TimelineItemContent,
VirtualTimelineItem,
};
use crate::{events::SyncTimelineEventWithoutContent, room::timeline::MembershipChange};
@@ -821,10 +822,7 @@ impl NewEventTimelineItem {
let edited = relations.replace.is_some();
let content = TimelineItemContent::Message(Message {
msgtype: c.msgtype,
in_reply_to: c.relates_to.and_then(|rel| match rel {
message::Relation::Reply { in_reply_to } => Some(in_reply_to.event_id),
_ => None,
}),
in_reply_to: c.relates_to.and_then(InReplyToDetails::from_relation),
edited,
});

View File

@@ -15,7 +15,7 @@
use std::{fmt, ops::Deref, sync::Arc};
use indexmap::IndexMap;
use matrix_sdk_base::deserialized_responses::EncryptionInfo;
use matrix_sdk_base::deserialized_responses::{EncryptionInfo, TimelineEvent};
use ruma::{
events::{
policy::rule::{
@@ -33,7 +33,7 @@ use ruma::{
history_visibility::RoomHistoryVisibilityEventContent,
join_rules::RoomJoinRulesEventContent,
member::{Change, RoomMemberEventContent},
message::MessageType,
message::{self, MessageType, Relation},
name::RoomNameEventContent,
pinned_events::RoomPinnedEventsEventContent,
power_levels::RoomPowerLevelsEventContent,
@@ -44,15 +44,16 @@ use ruma::{
},
space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
sticker::StickerEventContent,
AnyFullStateEventContent, AnySyncTimelineEvent, FullStateEventContent,
MessageLikeEventType, StateEventType,
AnyFullStateEventContent, AnyMessageLikeEventContent, AnySyncTimelineEvent,
AnyTimelineEvent, FullStateEventContent, MessageLikeEventType, StateEventType,
},
serde::Raw,
EventId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, OwnedMxcUri,
OwnedTransactionId, OwnedUserId, TransactionId, UserId,
};
use crate::Error;
use super::inner::ProfileProvider;
use crate::{Error, Result};
/// An item in the timeline that represents at least one event.
///
@@ -295,6 +296,11 @@ impl RemoteEventTimelineItem {
Self { reactions, ..self.clone() }
}
/// Clone the current event item, and update its `content`.
pub(super) fn with_content(&self, content: TimelineItemContent) -> Self {
Self { content, ..self.clone() }
}
/// Clone the current event item, change its `content` to
/// [`TimelineItemContent::RedactedMessage`], and reset its `reactions`.
pub(super) fn to_redacted(&self) -> Self {
@@ -364,6 +370,9 @@ pub enum TimelineDetails<T> {
/// The details are available.
Ready(T),
/// An error occurred when fetching the details.
Error(Arc<Error>),
}
/// The content of an [`EventTimelineItem`].
@@ -436,10 +445,7 @@ impl TimelineItemContent {
#[derive(Clone)]
pub struct Message {
pub(super) msgtype: MessageType,
// TODO: Add everything required to display the replied-to event, plus a
// 'loading' state that is entered at first, until the user requests the
// reply to be loaded.
pub(super) in_reply_to: Option<OwnedEventId>,
pub(super) in_reply_to: Option<InReplyToDetails>,
pub(super) edited: bool,
}
@@ -456,15 +462,19 @@ impl Message {
self.msgtype.body()
}
/// Get the event ID of the event this message is replying to, if any.
pub fn in_reply_to(&self) -> Option<&EventId> {
self.in_reply_to.as_deref()
/// Get the event this message is replying to, if any.
pub fn in_reply_to(&self) -> Option<&InReplyToDetails> {
self.in_reply_to.as_ref()
}
/// Get the edit state of this message (has been edited: `true` / `false`).
pub fn is_edited(&self) -> bool {
self.edited
}
pub(super) fn with_in_reply_to(&self, in_reply_to: InReplyToDetails) -> Self {
Self { in_reply_to: Some(in_reply_to), ..self.clone() }
}
}
impl fmt::Debug for Message {
@@ -475,6 +485,85 @@ impl fmt::Debug for Message {
}
}
/// Details about an event being replied to.
#[derive(Clone, Debug)]
pub struct InReplyToDetails {
/// The ID of the event.
pub event_id: OwnedEventId,
/// The details of the event.
///
/// Use [`Timeline::fetch_item_details`] to fetch the data if it is
/// unavailable. The `replies_nesting_level` field in
/// [`TimelineDetailsSettings`] decides if this should be fetched.
///
/// [`Timeline::fetch_item_details`]: super::Timeline::fetch_item_details
/// [`TimelineDetailsSettings`]: super::TimelineDetailsSettings
pub details: TimelineDetails<Box<RepliedToEvent>>,
}
impl InReplyToDetails {
pub(super) fn from_relation<C>(relation: Relation<C>) -> Option<Self> {
match relation {
message::Relation::Reply { in_reply_to } => {
Some(Self { event_id: in_reply_to.event_id, details: TimelineDetails::Unavailable })
}
_ => None,
}
}
}
/// An event that is replied to.
#[derive(Clone, Debug)]
pub struct RepliedToEvent {
pub(super) message: Message,
pub(super) sender: OwnedUserId,
pub(super) sender_profile: Profile,
}
impl RepliedToEvent {
/// Get the message of this event.
pub fn message(&self) -> &Message {
&self.message
}
/// Get the sender of this event.
pub fn sender(&self) -> &UserId {
&self.sender
}
/// Get the profile of the sender.
pub fn sender_profile(&self) -> &Profile {
&self.sender_profile
}
pub(super) async fn try_from_timeline_event<P: ProfileProvider>(
timeline_event: TimelineEvent,
profile_provider: &P,
) -> Result<Self> {
let event = match timeline_event.event.deserialize() {
Ok(AnyTimelineEvent::MessageLike(event)) => event,
_ => {
return Err(super::Error::UnsupportedEvent.into());
}
};
let Some(AnyMessageLikeEventContent::RoomMessage(c)) = event.original_content() else {
return Err(super::Error::UnsupportedEvent.into());
};
let message = Message {
msgtype: c.msgtype,
in_reply_to: c.relates_to.and_then(InReplyToDetails::from_relation),
edited: event.relations().replace.is_some(),
};
let sender = event.sender().to_owned();
let sender_profile = profile_provider.profile(&sender).await;
Ok(Self { message, sender, sender_profile })
}
}
/// Metadata about an `m.room.encrypted` event that could not be decrypted.
#[derive(Clone, Debug)]
pub enum EncryptedMessage {

View File

@@ -31,11 +31,13 @@ use super::{
update_read_marker, Flow, HandleEventResult, TimelineEventHandler, TimelineEventKind,
TimelineEventMetadata, TimelineItemPosition,
},
rfind_event_item, EventSendState, EventTimelineItem, Profile, TimelineItem,
rfind_event_by_id, rfind_event_item, EventSendState, EventTimelineItem, InReplyToDetails,
Message, Profile, RepliedToEvent, TimelineDetails, TimelineItem, TimelineItemContent,
};
use crate::{
events::SyncTimelineEventWithoutContent,
room::{self, timeline::event_item::RemoteEventTimelineItem},
Result,
};
#[derive(Debug)]
@@ -368,12 +370,93 @@ impl<P: ProfileProvider> TimelineInner<P> {
.await;
}
}
fn update_event_item(&self, index: usize, event_item: EventTimelineItem) {
self.items.lock_mut().set_cloned(index, Arc::new(TimelineItem::Event(event_item)))
}
}
impl TimelineInner {
pub(super) fn room(&self) -> &room::Common {
&self.profile_provider
}
pub(super) async fn fetch_in_reply_to_details(
&self,
index: usize,
mut item: RemoteEventTimelineItem,
) -> Result<RemoteEventTimelineItem> {
let TimelineItemContent::Message(message) = item.content.clone() else {
return Ok(item);
};
let Some(in_reply_to) = message.in_reply_to() else {
return Ok(item);
};
let details =
self.fetch_replied_to_event(index, &item, &message, &in_reply_to.event_id).await;
// We need to be sure to have the latest position of the event as it might have
// changed while waiting for the request.
let (index, _) = rfind_event_by_id(&self.items(), &item.event_id)
.ok_or(super::Error::RemoteEventNotInTimeline)?;
item = item.with_content(TimelineItemContent::Message(message.with_in_reply_to(
InReplyToDetails { event_id: in_reply_to.event_id.clone(), details },
)));
self.update_event_item(index, item.clone().into());
Ok(item)
}
async fn fetch_replied_to_event(
&self,
index: usize,
item: &RemoteEventTimelineItem,
message: &Message,
in_reply_to: &EventId,
) -> TimelineDetails<Box<RepliedToEvent>> {
if let Some((_, item)) = rfind_event_by_id(&self.items(), in_reply_to) {
let details = match item.content() {
TimelineItemContent::Message(message) => {
TimelineDetails::Ready(Box::new(RepliedToEvent {
message: message.clone(),
sender: item.sender().to_owned(),
sender_profile: item.sender_profile().clone(),
}))
}
_ => TimelineDetails::Error(Arc::new(super::Error::UnsupportedEvent.into())),
};
return details;
};
self.update_event_item(
index,
item.with_content(TimelineItemContent::Message(message.with_in_reply_to(
InReplyToDetails {
event_id: in_reply_to.to_owned(),
details: TimelineDetails::Pending,
},
)))
.into(),
);
match self.room().event(in_reply_to).await {
Ok(timeline_event) => {
match RepliedToEvent::try_from_timeline_event(
timeline_event,
&self.profile_provider,
)
.await
{
Ok(event) => TimelineDetails::Ready(Box::new(event)),
Err(e) => TimelineDetails::Error(Arc::new(e)),
}
}
Err(e) => TimelineDetails::Error(Arc::new(e)),
}
}
}
#[async_trait]

View File

@@ -28,6 +28,7 @@ use ruma::{
events::{fully_read::FullyReadEventContent, AnyMessageLikeEventContent},
EventId, MilliSecondsSinceUnixEpoch, TransactionId,
};
use thiserror::Error;
use tracing::{error, instrument, warn};
use super::Joined;
@@ -50,8 +51,9 @@ mod virtual_item;
pub use self::{
event_item::{
AnyOtherFullStateEventContent, BundledReactions, EncryptedMessage, EventSendState,
EventTimelineItem, MemberProfileChange, MembershipChange, Message, OtherState, Profile,
ReactionGroup, RoomMembershipChange, Sticker, TimelineDetails, TimelineItemContent,
EventTimelineItem, InReplyToDetails, MemberProfileChange, MembershipChange, Message,
OtherState, Profile, ReactionGroup, RepliedToEvent, RoomMembershipChange, Sticker,
TimelineDetails, TimelineItemContent,
},
pagination::{PaginationOptions, PaginationOutcome},
virtual_item::VirtualTimelineItem,
@@ -389,6 +391,36 @@ impl Timeline {
};
self.inner.update_event_send_state(&txn_id, send_state);
}
/// Fetch unavailable details about the event with the given ID.
///
/// This method only works for IDs of [`RemoteEventTimelineItem`]s, to
/// prevent losing details when a local echo is replaced by its remote
/// echo.
///
/// This method tries to make all the requests it can. If an error is
/// encountered for a given request, it is forwarded with the
/// [`TimelineDetails::Error`] variant.
///
/// # Arguments
///
/// * `event_id` - The event ID of the event to fetch details for.
///
/// # Errors
///
/// Returns an error if the identifier doesn't match any event with a remote
/// echo in the timeline, or if the event is removed from the timeline
/// before all requests are handled.
#[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
pub async fn fetch_event_details(&self, event_id: &EventId) -> Result<()> {
let (index, item) = rfind_event_by_id(&self.inner.items(), event_id)
.and_then(|(pos, item)| item.as_remote().map(|item| (pos, item.clone())))
.ok_or(Error::RemoteEventNotInTimeline)?;
self.inner.fetch_in_reply_to_details(index, item).await?;
Ok(())
}
}
/// A single entry in timeline.
@@ -473,3 +505,16 @@ fn rfind_event_by_id<'a>(
fn find_read_marker(items: &[Arc<TimelineItem>]) -> Option<usize> {
items.iter().rposition(|item| item.is_read_marker())
}
/// Errors specific to the timeline.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
/// The requested event with a remote echo is not in the timeline.
#[error("Event with remote echo not found in timeline")]
RemoteEventNotInTimeline,
/// The event is currently unsupported for this use case.
#[error("Unsupported event")]
UnsupportedEvent,
}

View File

@@ -814,8 +814,8 @@ impl SlidingSync {
}
/// Lookup a specific room
pub fn get_room(&self, room_id: OwnedRoomId) -> Option<SlidingSyncRoom> {
self.rooms.lock_ref().get(&room_id).cloned()
pub fn get_room(&self, room_id: &RoomId) -> Option<SlidingSyncRoom> {
self.rooms.lock_ref().get(room_id).cloned()
}
fn update_to_device_since(&self, since: String) {

View File

@@ -8,10 +8,11 @@ use futures_util::StreamExt;
use matrix_sdk::{
config::SyncSettings,
room::timeline::{
AnyOtherFullStateEventContent, EventSendState, PaginationOptions, TimelineItemContent,
VirtualTimelineItem,
AnyOtherFullStateEventContent, Error as TimelineError, EventSendState, PaginationOptions,
TimelineDetails, TimelineItemContent, VirtualTimelineItem,
},
ruma::MilliSecondsSinceUnixEpoch,
Error,
};
use matrix_sdk_common::executor::spawn;
use matrix_sdk_test::{
@@ -553,3 +554,163 @@ async fn read_marker() {
assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value);
assert_matches!(marker.as_virtual().unwrap(), VirtualTimelineItem::ReadMarker);
}
#[async_test]
async fn in_reply_to_details() {
let room_id = room_id!("!a98sd12bjh:example.org");
let (client, server) = logged_in_client().await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
let mut ev_builder = EventBuilder::new();
ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
mock_sync(&server, ev_builder.build_json_sync_response(), None).await;
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
server.reset().await;
let room = client.get_room(room_id).unwrap();
let timeline = room.timeline().await;
let mut timeline_stream = timeline.signal().to_stream();
// The event doesn't exist.
assert_matches!(
timeline.fetch_event_details(event_id!("$fakeevent")).await,
Err(Error::Timeline(TimelineError::RemoteEventNotInTimeline))
);
ev_builder.add_joined_room(
JoinedRoomBuilder::new(room_id)
.add_timeline_event(TimelineTestEvent::Custom(json!({
"content": {
"body": "hello",
"msgtype": "m.text",
},
"event_id": "$event1",
"origin_server_ts": 152037280,
"sender": "@alice:example.org",
"type": "m.room.message",
})))
.add_timeline_event(TimelineTestEvent::Custom(json!({
"content": {
"body": "hello to you too",
"msgtype": "m.text",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$event1",
},
},
},
"event_id": "$event2",
"origin_server_ts": 152045456,
"sender": "@bob:example.org",
"type": "m.room.message",
}))),
);
mock_sync(&server, ev_builder.build_json_sync_response(), None).await;
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
server.reset().await;
let _day_divider =
assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value);
let first =
assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value);
assert_matches!(first.as_event().unwrap().content(), TimelineItemContent::Message(_));
let second =
assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value);
let second_event = second.as_event().unwrap().as_remote().unwrap();
let message =
assert_matches!(&second_event.content, TimelineItemContent::Message(message) => message);
let in_reply_to = message.in_reply_to().unwrap();
assert_eq!(in_reply_to.event_id, event_id!("$event1"));
assert_matches!(in_reply_to.details, TimelineDetails::Unavailable);
// Fetch details locally first.
timeline.fetch_event_details(&second_event.event_id).await.unwrap();
let second = assert_matches!(timeline_stream.next().await, Some(VecDiff::UpdateAt { index: 2, value }) => value);
let message = assert_matches!(second.as_event().unwrap().content(), TimelineItemContent::Message(message) => message);
assert_matches!(message.in_reply_to().unwrap().details, TimelineDetails::Ready(_));
ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event(
TimelineTestEvent::Custom(json!({
"content": {
"body": "you were right",
"msgtype": "m.text",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$remoteevent",
},
},
},
"event_id": "$event3",
"origin_server_ts": 152046694,
"sender": "@bob:example.org",
"type": "m.room.message",
})),
));
mock_sync(&server, ev_builder.build_json_sync_response(), None).await;
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
server.reset().await;
let third =
assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value);
let third_event = third.as_event().unwrap().as_remote().unwrap();
let message =
assert_matches!(&third_event.content, TimelineItemContent::Message(message) => message);
let in_reply_to = message.in_reply_to().unwrap();
assert_eq!(in_reply_to.event_id, event_id!("$remoteevent"));
assert_matches!(in_reply_to.details, TimelineDetails::Unavailable);
Mock::given(method("GET"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/\$remoteevent"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
"errcode": "M_NOT_FOUND",
"error": "Event not found.",
})))
.expect(1)
.mount(&server)
.await;
// Fetch details remotely if we can't find them locally.
timeline.fetch_event_details(&third_event.event_id).await.unwrap();
server.reset().await;
let third = assert_matches!(timeline_stream.next().await, Some(VecDiff::UpdateAt { index: 3, value }) => value);
let message = assert_matches!(third.as_event().unwrap().content(), TimelineItemContent::Message(message) => message);
assert_matches!(message.in_reply_to().unwrap().details, TimelineDetails::Pending);
let third = assert_matches!(timeline_stream.next().await, Some(VecDiff::UpdateAt { index: 3, value }) => value);
let message = assert_matches!(third.as_event().unwrap().content(), TimelineItemContent::Message(message) => message);
assert_matches!(message.in_reply_to().unwrap().details, TimelineDetails::Error(_));
Mock::given(method("GET"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/\$remoteevent"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"content": {
"body": "Alice is gonna arrive soon",
"msgtype": "m.text",
},
"room_id": room_id,
"event_id": "$event0",
"origin_server_ts": 152024004,
"sender": "@admin:example.org",
"type": "m.room.message",
})))
.expect(1)
.mount(&server)
.await;
timeline.fetch_event_details(&third_event.event_id).await.unwrap();
let third = assert_matches!(timeline_stream.next().await, Some(VecDiff::UpdateAt { index: 3, value }) => value);
let message = assert_matches!(third.as_event().unwrap().content(), TimelineItemContent::Message(message) => message);
assert_matches!(message.in_reply_to().unwrap().details, TimelineDetails::Pending);
let third = assert_matches!(timeline_stream.next().await, Some(VecDiff::UpdateAt { index: 3, value }) => value);
let message = assert_matches!(third.as_event().unwrap().content(), TimelineItemContent::Message(message) => message);
assert_matches!(message.in_reply_to().unwrap().details, TimelineDetails::Ready(_));
}