mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-18 13:40:55 -04:00
Allow edit and reply to work also for events that have not yet been paginated (#3553)
Fixes #3538 The current implementation for send_reply and edit only work with timeline items that have already been paginated. However given the fact that by restoring drafts, we may restore a reply to an event for timeline where such event has not been paginated, sending such reply would fail (same for the edit event). So I reworked a bit the code here to use. only the event id, and reuse the existing timeline if available, otherwise we can fetch the event and synthethise the content and still be able to successfully send the event. This is the third part of the breakdown of the following PR: https://github.com/matrix-org/matrix-rust-sdk/pull/3439
This commit is contained in:
@@ -454,10 +454,17 @@ impl Timeline {
|
||||
pub async fn send_reply(
|
||||
&self,
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
reply_item: Arc<EventTimelineItem>,
|
||||
event_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
let replied_to_info = self
|
||||
.inner
|
||||
.replied_to_info_from_event_id(&event_id)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
|
||||
self.inner
|
||||
.send_reply((*msg).clone(), &reply_item.0, ForwardThread::Yes)
|
||||
.send_reply((*msg).clone(), replied_to_info, ForwardThread::Yes)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
Ok(())
|
||||
@@ -466,10 +473,17 @@ impl Timeline {
|
||||
pub async fn edit(
|
||||
&self,
|
||||
new_content: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
edit_item: Arc<EventTimelineItem>,
|
||||
event_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
let edit_info = self
|
||||
.inner
|
||||
.edit_info_from_event_id(&event_id)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
|
||||
self.inner
|
||||
.edit((*new_content).clone(), &edit_item.0)
|
||||
.edit((*new_content).clone(), edit_info)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
Ok(())
|
||||
|
||||
@@ -88,6 +88,10 @@ pub struct UnsupportedReplyItem(UnsupportedReplyItemInner);
|
||||
impl UnsupportedReplyItem {
|
||||
pub(super) const MISSING_EVENT_ID: Self = Self(UnsupportedReplyItemInner::MissingEventId);
|
||||
pub(super) const MISSING_JSON: Self = Self(UnsupportedReplyItemInner::MissingJson);
|
||||
pub(super) const MISSING_EVENT: Self = Self(UnsupportedReplyItemInner::MissingEvent);
|
||||
pub(super) const FAILED_TO_DESERIALIZE_EVENT: Self =
|
||||
Self(UnsupportedReplyItemInner::FailedToDeserializeEvent);
|
||||
pub(super) const STATE_EVENT: Self = Self(UnsupportedReplyItemInner::StateEvent);
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -103,6 +107,12 @@ enum UnsupportedReplyItemInner {
|
||||
MissingEventId,
|
||||
#[error("redacted events whose JSON form isn't available can't be replied")]
|
||||
MissingJson,
|
||||
#[error("event to reply to not found")]
|
||||
MissingEvent,
|
||||
#[error("failed to deserialize event to reply to")]
|
||||
FailedToDeserializeEvent,
|
||||
#[error("tried to reply to a state event")]
|
||||
StateEvent,
|
||||
}
|
||||
|
||||
#[derive(Error)]
|
||||
@@ -114,6 +124,9 @@ impl UnsupportedEditItem {
|
||||
pub(super) const NOT_ROOM_MESSAGE: Self = Self(UnsupportedEditItemInner::NotRoomMessage);
|
||||
pub(super) const NOT_POLL_EVENT: Self = Self(UnsupportedEditItemInner::NotPollEvent);
|
||||
pub(super) const NOT_OWN_EVENT: Self = Self(UnsupportedEditItemInner::NotOwnEvent);
|
||||
pub(super) const MISSING_EVENT: Self = Self(UnsupportedEditItemInner::MissingEvent);
|
||||
pub(super) const FAILED_TO_DESERIALIZE_EVENT: Self =
|
||||
Self(UnsupportedEditItemInner::FailedToDeserializeEvent);
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -133,6 +146,10 @@ enum UnsupportedEditItemInner {
|
||||
NotPollEvent,
|
||||
#[error("tried to edit another user's event")]
|
||||
NotOwnEvent,
|
||||
#[error("event to edit not found")]
|
||||
MissingEvent,
|
||||
#[error("failed to deserialize event to edit")]
|
||||
FailedToDeserializeEvent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
||||
@@ -45,6 +45,7 @@ pub(super) use self::{
|
||||
local::LocalEventTimelineItem,
|
||||
remote::{RemoteEventOrigin, RemoteEventTimelineItem},
|
||||
};
|
||||
use super::{EditInfo, RepliedToInfo, ReplyContent, UnsupportedEditItem, UnsupportedReplyItem};
|
||||
|
||||
/// An item in the timeline that represents at least one event.
|
||||
///
|
||||
@@ -421,6 +422,47 @@ impl EventTimelineItem {
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives the information needed to reply to the event of the item.
|
||||
pub fn replied_to_info(&self) -> Result<RepliedToInfo, UnsupportedReplyItem> {
|
||||
let reply_content = match self.content() {
|
||||
TimelineItemContent::Message(msg) => ReplyContent::Message(msg.to_owned()),
|
||||
_ => {
|
||||
let Some(raw_event) = self.latest_json() else {
|
||||
return Err(UnsupportedReplyItem::MISSING_JSON);
|
||||
};
|
||||
|
||||
ReplyContent::Raw(raw_event.clone())
|
||||
}
|
||||
};
|
||||
|
||||
let Some(event_id) = self.event_id() else {
|
||||
return Err(UnsupportedReplyItem::MISSING_EVENT_ID);
|
||||
};
|
||||
|
||||
Ok(RepliedToInfo {
|
||||
event_id: event_id.to_owned(),
|
||||
sender: self.sender().to_owned(),
|
||||
timestamp: self.timestamp(),
|
||||
content: reply_content,
|
||||
})
|
||||
}
|
||||
|
||||
/// Gives the information needed to edit the event of the item.
|
||||
pub fn edit_info(&self) -> Result<EditInfo, UnsupportedEditItem> {
|
||||
if !self.is_own() {
|
||||
return Err(UnsupportedEditItem::NOT_OWN_EVENT);
|
||||
}
|
||||
// Early returns here must be in sync with
|
||||
// `EventTimelineItem::can_be_edited`
|
||||
let Some(event_id) = self.event_id() else {
|
||||
return Err(UnsupportedEditItem::MISSING_EVENT_ID);
|
||||
};
|
||||
let TimelineItemContent::Message(original_content) = self.content() else {
|
||||
return Err(UnsupportedEditItem::NOT_ROOM_MESSAGE);
|
||||
};
|
||||
Ok(EditInfo { event_id: event_id.to_owned(), original_message: original_content.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalEventTimelineItem> for EventTimelineItemKind {
|
||||
|
||||
@@ -50,10 +50,11 @@ use ruma::{
|
||||
},
|
||||
redaction::RoomRedactionEventContent,
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
},
|
||||
uint, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, RoomVersionId,
|
||||
TransactionId, UserId,
|
||||
serde::Raw,
|
||||
uint, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId,
|
||||
RoomVersionId, TransactionId, UserId,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tracing::{error, instrument, trace, warn};
|
||||
@@ -110,6 +111,37 @@ use self::{
|
||||
util::rfind_event_by_id,
|
||||
};
|
||||
|
||||
/// Information needed to edit an event.
|
||||
#[derive(Debug)]
|
||||
pub struct EditInfo {
|
||||
/// The event ID of the event that needs editing.
|
||||
event_id: OwnedEventId,
|
||||
/// The original content of the event that needs editing.
|
||||
original_message: Message,
|
||||
}
|
||||
|
||||
/// Information needed to reply to an event.
|
||||
#[derive(Debug)]
|
||||
pub struct RepliedToInfo {
|
||||
/// The event ID of the event to reply to.
|
||||
event_id: OwnedEventId,
|
||||
/// The sender of the event to reply to.
|
||||
sender: OwnedUserId,
|
||||
/// The timestamp of the event to reply to.
|
||||
timestamp: MilliSecondsSinceUnixEpoch,
|
||||
/// The content of the event to reply to.
|
||||
content: ReplyContent,
|
||||
}
|
||||
|
||||
/// The content of a reply.
|
||||
#[derive(Debug)]
|
||||
enum ReplyContent {
|
||||
/// Content of a message event.
|
||||
Message(Message),
|
||||
/// Content of any other kind of event stored as raw.
|
||||
Raw(Raw<AnySyncTimelineEvent>),
|
||||
}
|
||||
|
||||
/// A high-level view into a regular¹ room's contents.
|
||||
///
|
||||
/// ¹ This type is meant to be used in the context of rooms without a
|
||||
@@ -309,69 +341,60 @@ impl Timeline {
|
||||
/// change. Please check [`EventTimelineItem::can_be_replied_to`] to decide
|
||||
/// whether to render a reply button.
|
||||
///
|
||||
/// The sender of `reply_item` will be added to the mentions of the reply if
|
||||
/// and only if `reply_item` has not been written by the sender.
|
||||
/// The sender will be added to the mentions of the reply if
|
||||
/// and only if the event has not been written by the sender.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `content` - The content of the reply
|
||||
///
|
||||
/// * `reply_item` - The event item you want to reply to
|
||||
/// * `replied_to_info` - A wrapper that contains the event ID, sender,
|
||||
/// content and timestamp of the event to reply to
|
||||
///
|
||||
/// * `forward_thread` - Usually `Yes`, unless you explicitly want to the
|
||||
/// reply to show up in the main timeline even though the `reply_item` is
|
||||
/// part of a thread
|
||||
#[instrument(skip(self, content, reply_item))]
|
||||
#[instrument(skip(self, content, replied_to_info))]
|
||||
pub async fn send_reply(
|
||||
&self,
|
||||
content: RoomMessageEventContentWithoutRelation,
|
||||
reply_item: &EventTimelineItem,
|
||||
replied_to_info: RepliedToInfo,
|
||||
forward_thread: ForwardThread,
|
||||
) -> Result<(), SendEventError> {
|
||||
// Error returns here must be in sync with
|
||||
// `EventTimelineItem::can_be_replied_to`
|
||||
let Some(event_id) = reply_item.event_id() else {
|
||||
return Err(UnsupportedReplyItem::MISSING_EVENT_ID.into());
|
||||
};
|
||||
) -> Result<(), RoomSendQueueError> {
|
||||
let event_id = replied_to_info.event_id;
|
||||
|
||||
// [The specification](https://spec.matrix.org/v1.10/client-server-api/#user-and-room-mentions) says:
|
||||
//
|
||||
// > Users should not add their own Matrix ID to the `m.mentions` property as
|
||||
// > outgoing messages cannot self-notify.
|
||||
//
|
||||
// If `reply_item` has been written by the current user, let's toggle to
|
||||
// If the replied to event has been written by the current user, let's toggle to
|
||||
// `AddMentions::No`.
|
||||
let mention_the_sender = if self.room().own_user_id() == reply_item.sender {
|
||||
let mention_the_sender = if self.room().own_user_id() == replied_to_info.sender {
|
||||
AddMentions::No
|
||||
} else {
|
||||
AddMentions::Yes
|
||||
};
|
||||
|
||||
let content = match reply_item.content() {
|
||||
TimelineItemContent::Message(msg) => {
|
||||
let content = match replied_to_info.content {
|
||||
ReplyContent::Message(msg) => {
|
||||
let event = OriginalRoomMessageEvent {
|
||||
event_id: event_id.to_owned(),
|
||||
sender: reply_item.sender().to_owned(),
|
||||
origin_server_ts: reply_item.timestamp(),
|
||||
sender: replied_to_info.sender,
|
||||
origin_server_ts: replied_to_info.timestamp,
|
||||
room_id: self.room().room_id().to_owned(),
|
||||
content: msg.to_content(),
|
||||
unsigned: Default::default(),
|
||||
};
|
||||
content.make_reply_to(&event, forward_thread, mention_the_sender)
|
||||
}
|
||||
_ => {
|
||||
let Some(raw_event) = reply_item.latest_json() else {
|
||||
return Err(UnsupportedReplyItem::MISSING_JSON.into());
|
||||
};
|
||||
|
||||
content.make_reply_to_raw(
|
||||
raw_event,
|
||||
event_id.to_owned(),
|
||||
self.room().room_id(),
|
||||
forward_thread,
|
||||
mention_the_sender,
|
||||
)
|
||||
}
|
||||
ReplyContent::Raw(raw_event) => content.make_reply_to_raw(
|
||||
&raw_event,
|
||||
event_id.to_owned(),
|
||||
self.room().room_id(),
|
||||
forward_thread,
|
||||
mention_the_sender,
|
||||
),
|
||||
};
|
||||
|
||||
self.send(content.into()).await?;
|
||||
@@ -379,6 +402,63 @@ impl Timeline {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gives the information needed to reply to an event from an event id.
|
||||
pub async fn replied_to_info_from_event_id(
|
||||
&self,
|
||||
event_id: &EventId,
|
||||
) -> Result<RepliedToInfo, UnsupportedReplyItem> {
|
||||
if let Some(timeline_item) = self.item_by_event_id(event_id).await {
|
||||
return timeline_item.replied_to_info();
|
||||
}
|
||||
|
||||
let event = match self.room().event(event_id).await {
|
||||
Ok(event) => event,
|
||||
Err(error) => {
|
||||
error!("Failed to fetch event with ID {event_id} with error: {error}");
|
||||
return Err(UnsupportedReplyItem::MISSING_EVENT);
|
||||
}
|
||||
};
|
||||
|
||||
// We need to get the content and we can do that by casting
|
||||
// the event as a `AnySyncTimelineEvent` which is the same as a
|
||||
// `AnyTimelineEvent`, but without the `room_id` field.
|
||||
// The cast is valid because we are just losing track of such field.
|
||||
let raw_sync_event: Raw<AnySyncTimelineEvent> = event.event.cast();
|
||||
let sync_event = match raw_sync_event.deserialize() {
|
||||
Ok(event) => event,
|
||||
Err(error) => {
|
||||
error!("Failed to deserialize event with ID {event_id} with error: {error}");
|
||||
return Err(UnsupportedReplyItem::FAILED_TO_DESERIALIZE_EVENT);
|
||||
}
|
||||
};
|
||||
|
||||
let reply_content = match &sync_event {
|
||||
AnySyncTimelineEvent::MessageLike(message_like_event) => {
|
||||
if let AnySyncMessageLikeEvent::RoomMessage(message_event) = message_like_event {
|
||||
if let Some(original_message) = message_event.as_original() {
|
||||
ReplyContent::Message(Message::from_event(
|
||||
original_message.content.clone(),
|
||||
message_like_event.relations(),
|
||||
&self.items().await,
|
||||
))
|
||||
} else {
|
||||
ReplyContent::Raw(raw_sync_event)
|
||||
}
|
||||
} else {
|
||||
ReplyContent::Raw(raw_sync_event)
|
||||
}
|
||||
}
|
||||
AnySyncTimelineEvent::State(_) => return Err(UnsupportedReplyItem::STATE_EVENT),
|
||||
};
|
||||
|
||||
Ok(RepliedToInfo {
|
||||
event_id: event_id.to_owned(),
|
||||
sender: sync_event.sender().to_owned(),
|
||||
timestamp: sync_event.origin_server_ts(),
|
||||
content: reply_content,
|
||||
})
|
||||
}
|
||||
|
||||
/// Send an edit to the given event.
|
||||
///
|
||||
/// Currently only supports `m.room.message` events whose event ID is known.
|
||||
@@ -388,24 +468,16 @@ impl Timeline {
|
||||
///
|
||||
/// * `new_content` - The content of the reply
|
||||
///
|
||||
/// * `edit_item` - The event item you want to edit
|
||||
/// * `edit_info` - A wrapper that contains the event ID and the content of
|
||||
/// the event to edit
|
||||
#[instrument(skip(self, new_content))]
|
||||
pub async fn edit(
|
||||
&self,
|
||||
new_content: RoomMessageEventContentWithoutRelation,
|
||||
edit_item: &EventTimelineItem,
|
||||
) -> Result<(), SendEventError> {
|
||||
// Early returns here must be in sync with `EventTimelineItem::is_editable`.
|
||||
if !edit_item.is_own() {
|
||||
return Err(UnsupportedEditItem::NOT_OWN_EVENT.into());
|
||||
}
|
||||
let Some(event_id) = edit_item.event_id() else {
|
||||
return Err(UnsupportedEditItem::MISSING_EVENT_ID.into());
|
||||
};
|
||||
|
||||
let TimelineItemContent::Message(original_content) = edit_item.content() else {
|
||||
return Err(UnsupportedEditItem::NOT_ROOM_MESSAGE.into());
|
||||
};
|
||||
edit_info: EditInfo,
|
||||
) -> Result<(), RoomSendQueueError> {
|
||||
let original_content = edit_info.original_message;
|
||||
let event_id = edit_info.event_id;
|
||||
|
||||
let replied_to_message =
|
||||
original_content.in_reply_to().and_then(|details| match &details.event {
|
||||
@@ -437,6 +509,58 @@ impl Timeline {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Give the information needed to edit an event from an event id.
|
||||
pub async fn edit_info_from_event_id(
|
||||
&self,
|
||||
event_id: &EventId,
|
||||
) -> Result<EditInfo, UnsupportedEditItem> {
|
||||
if let Some(timeline_item) = self.item_by_event_id(event_id).await {
|
||||
return timeline_item.edit_info();
|
||||
}
|
||||
|
||||
let event = match self.room().event(event_id).await {
|
||||
Ok(event) => event,
|
||||
Err(error) => {
|
||||
error!("Failed to fetch event with ID {event_id} with error: {error}");
|
||||
return Err(UnsupportedEditItem::MISSING_EVENT);
|
||||
}
|
||||
};
|
||||
|
||||
// We need to get the content and we can do that by casting
|
||||
// the event as a `AnySyncTimelineEvent` which is the same as a
|
||||
// `AnyTimelineEvent`, but without the `room_id` field.
|
||||
// The cast is valid because we are just losing track of such field.
|
||||
let raw_sync_event: Raw<AnySyncTimelineEvent> = event.event.cast();
|
||||
let event = match raw_sync_event.deserialize() {
|
||||
Ok(event) => event,
|
||||
Err(error) => {
|
||||
error!("Failed to deserialize event with ID {event_id} with error: {error}");
|
||||
return Err(UnsupportedEditItem::FAILED_TO_DESERIALIZE_EVENT);
|
||||
}
|
||||
};
|
||||
|
||||
if event.sender() != self.room().own_user_id() {
|
||||
return Err(UnsupportedEditItem::NOT_OWN_EVENT);
|
||||
};
|
||||
|
||||
if let AnySyncTimelineEvent::MessageLike(message_like_event) = &event {
|
||||
if let AnySyncMessageLikeEvent::RoomMessage(message_event) = message_like_event {
|
||||
if let Some(original_message) = message_event.as_original() {
|
||||
let message = Message::from_event(
|
||||
original_message.content.clone(),
|
||||
message_like_event.relations(),
|
||||
&self.items().await,
|
||||
);
|
||||
return Ok(EditInfo {
|
||||
event_id: event_id.to_owned(),
|
||||
original_message: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(UnsupportedEditItem::NOT_ROOM_MESSAGE)
|
||||
}
|
||||
|
||||
pub async fn edit_poll(
|
||||
&self,
|
||||
fallback_text: impl Into<String>,
|
||||
|
||||
@@ -42,7 +42,7 @@ use serde_json::json;
|
||||
use stream_assert::assert_next_matches;
|
||||
use tokio::{task::yield_now, time::sleep};
|
||||
use wiremock::{
|
||||
matchers::{method, path_regex},
|
||||
matchers::{header, method, path_regex},
|
||||
Mock, ResponseTemplate,
|
||||
};
|
||||
|
||||
@@ -201,8 +201,9 @@ async fn test_send_edit() {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let edit_info = hello_world_item.edit_info().unwrap();
|
||||
timeline
|
||||
.edit(RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!"), &hello_world_item)
|
||||
.edit(RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!"), edit_info)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -291,8 +292,9 @@ async fn test_send_reply_edit() {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let edit_info = reply_item.edit_info().unwrap();
|
||||
timeline
|
||||
.edit(RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!"), &reply_item)
|
||||
.edit(RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!"), edit_info)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -410,3 +412,89 @@ async fn test_send_edit_poll() {
|
||||
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_edit_when_timeline_is_clear() {
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let (client, server) = logged_in_client_with_server().await;
|
||||
let event_builder = EventBuilder::new();
|
||||
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
|
||||
|
||||
mock_sync(&server, sync_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.unwrap();
|
||||
let (_, mut timeline_stream) =
|
||||
timeline.subscribe_filter_map(|item| item.as_event().cloned()).await;
|
||||
|
||||
let raw_original_event = event_builder.make_sync_message_event_with_id(
|
||||
// Same user as the logged_in_client
|
||||
user_id!("@example:localhost"),
|
||||
event_id!("$original_event"),
|
||||
RoomMessageEventContent::text_plain("Hello, World!"),
|
||||
);
|
||||
sync_builder.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(raw_original_event.clone()),
|
||||
);
|
||||
|
||||
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
|
||||
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
|
||||
server.reset().await;
|
||||
|
||||
let hello_world_item =
|
||||
assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value);
|
||||
let hello_world_message = hello_world_item.content().as_message().unwrap();
|
||||
assert!(!hello_world_message.is_edited());
|
||||
assert!(hello_world_item.is_editable());
|
||||
|
||||
// Clear the timeline to make sure the old item does not need to be
|
||||
// available in it for the edit to work.
|
||||
timeline.clear().await;
|
||||
assert_next_matches!(timeline_stream, VectorDiff::Clear);
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
Mock::given(method("PUT"))
|
||||
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$edit_event" })),
|
||||
)
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Since we assume we can't use the timeline item directly in this use case, the
|
||||
// API will fetch the event from the server directly so we need to mock the
|
||||
// response.
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(raw_original_event.json()))
|
||||
.expect(1)
|
||||
.named("event_1")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let edit_info =
|
||||
timeline.edit_info_from_event_id(hello_world_item.event_id().unwrap()).await.unwrap();
|
||||
timeline
|
||||
.edit(RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!"), edit_info)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Since verifying the content would mean mocking the sliding sync response with
|
||||
// what we are already expecting, because this test would require to paginate
|
||||
// again the timeline, testing the content change would not be meaningful.
|
||||
// Use an integration test for the full case.
|
||||
|
||||
// The response to the mocked endpoint does not generate further timeline
|
||||
// updates, so just wait for a bit before verifying that the endpoint was
|
||||
// called.
|
||||
sleep(Duration::from_millis(200)).await;
|
||||
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use matrix_sdk_ui::timeline::{
|
||||
use ruma::{
|
||||
assign, event_id,
|
||||
events::{
|
||||
reaction::RedactedReactionEventContent,
|
||||
relation::InReplyTo,
|
||||
room::message::{
|
||||
AddMentions, ForwardThread, OriginalRoomMessageEvent, Relation, ReplyWithinThread,
|
||||
@@ -314,10 +315,11 @@ async fn test_send_reply() {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let replied_to_info = event_from_bob.replied_to_info().unwrap();
|
||||
timeline
|
||||
.send_reply(
|
||||
RoomMessageEventContentWithoutRelation::text_plain("Replying to Bob"),
|
||||
&event_from_bob,
|
||||
replied_to_info,
|
||||
ForwardThread::Yes,
|
||||
)
|
||||
.await
|
||||
@@ -421,10 +423,11 @@ async fn test_send_reply_to_self() {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let replied_to_info = event_from_self.replied_to_info().unwrap();
|
||||
timeline
|
||||
.send_reply(
|
||||
RoomMessageEventContentWithoutRelation::text_plain("Replying to self"),
|
||||
&event_from_self,
|
||||
replied_to_info,
|
||||
ForwardThread::Yes,
|
||||
)
|
||||
.await
|
||||
@@ -511,10 +514,11 @@ async fn test_send_reply_to_threaded() {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let replied_to_info = hello_world_item.replied_to_info().unwrap();
|
||||
timeline
|
||||
.send_reply(
|
||||
RoomMessageEventContentWithoutRelation::text_plain("Hello, Bob!"),
|
||||
&hello_world_item,
|
||||
replied_to_info,
|
||||
ForwardThread::Yes,
|
||||
)
|
||||
.await
|
||||
@@ -557,3 +561,230 @@ async fn test_send_reply_to_threaded() {
|
||||
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_reply_with_event_id() {
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let (client, server) = logged_in_client_with_server().await;
|
||||
let event_builder = EventBuilder::new();
|
||||
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
|
||||
|
||||
mock_sync(&server, sync_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.unwrap();
|
||||
let (_, mut timeline_stream) =
|
||||
timeline.subscribe_filter_map(|item| item.as_event().cloned()).await;
|
||||
|
||||
let event_id_from_bob = event_id!("$event_from_bob");
|
||||
let raw_event_from_bob = event_builder.make_sync_message_event_with_id(
|
||||
&BOB,
|
||||
event_id_from_bob,
|
||||
RoomMessageEventContent::text_plain("Hello from Bob"),
|
||||
);
|
||||
sync_builder.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(raw_event_from_bob.clone()),
|
||||
);
|
||||
|
||||
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
|
||||
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
|
||||
server.reset().await;
|
||||
|
||||
let event_from_bob =
|
||||
assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value);
|
||||
assert_eq!(event_from_bob.event_id().unwrap(), event_id_from_bob);
|
||||
|
||||
// Clear the timeline to make sure the old item does not need to be
|
||||
// available in it for the reply to work.
|
||||
timeline.clear().await;
|
||||
assert_next_matches!(timeline_stream, VectorDiff::Clear);
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
|
||||
// Now, let's reply to a message sent by `BOB`.
|
||||
Mock::given(method("PUT"))
|
||||
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*"))
|
||||
.respond_with(move |req: &Request| {
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
let reply_event = req
|
||||
.body_json::<RoomMessageEventContent>()
|
||||
.expect("Failed to deserialize the event");
|
||||
|
||||
assert_matches!(reply_event.relates_to, Some(Relation::Reply { in_reply_to: InReplyTo { event_id, .. } }) => {
|
||||
assert_eq!(event_id, event_id_from_bob);
|
||||
});
|
||||
assert_matches!(reply_event.mentions, Some(Mentions { user_ids, room: false, .. }) => {
|
||||
assert_eq!(user_ids.len(), 1);
|
||||
assert!(user_ids.contains(*BOB));
|
||||
});
|
||||
|
||||
ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$reply_event" }))
|
||||
})
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Since we assume we can't use the timeline item directly in this use case, the
|
||||
// API will fetch the event from the server directly so we need to mock the
|
||||
// response.
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(raw_event_from_bob.json()))
|
||||
.expect(1)
|
||||
.named("event_1")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let replied_to_info = timeline.replied_to_info_from_event_id(event_id_from_bob).await.unwrap();
|
||||
timeline
|
||||
.send_reply(
|
||||
RoomMessageEventContentWithoutRelation::text_plain("Replying to Bob"),
|
||||
replied_to_info,
|
||||
ForwardThread::Yes,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Let the sending queue handle the event.
|
||||
yield_now().await;
|
||||
|
||||
let reply_item = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value);
|
||||
|
||||
assert_matches!(reply_item.send_state(), Some(EventSendState::NotSentYet));
|
||||
let reply_message = reply_item.content().as_message().unwrap();
|
||||
assert_eq!(reply_message.body(), "Replying to Bob");
|
||||
let in_reply_to = reply_message.in_reply_to().unwrap();
|
||||
assert_eq!(in_reply_to.event_id, event_id_from_bob);
|
||||
|
||||
let diff = timeout(timeline_stream.next(), Duration::from_secs(1)).await.unwrap().unwrap();
|
||||
assert_let!(VectorDiff::Set { index: 0, value: reply_item_remote_echo } = diff);
|
||||
|
||||
assert_matches!(reply_item_remote_echo.send_state(), Some(EventSendState::Sent { .. }));
|
||||
let reply_message = reply_item_remote_echo.content().as_message().unwrap();
|
||||
assert_eq!(reply_message.body(), "Replying to Bob");
|
||||
let in_reply_to = reply_message.in_reply_to().unwrap();
|
||||
assert_eq!(in_reply_to.event_id, event_id_from_bob);
|
||||
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_reply_with_event_id_that_is_redacted() {
|
||||
// This test checks if is possible to reply to a redacted event that is not in
|
||||
// the timeline. The event id will go through a process where the event is
|
||||
// fetched and the content will be extracted and deserialised to be used in
|
||||
// the reply.
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let (client, server) = logged_in_client_with_server().await;
|
||||
let event_builder = EventBuilder::new();
|
||||
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
|
||||
|
||||
mock_sync(&server, sync_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.unwrap();
|
||||
let (_, mut timeline_stream) =
|
||||
timeline.subscribe_filter_map(|item| item.as_event().cloned()).await;
|
||||
|
||||
let redacted_event_id_from_bob = event_id!("$event_from_bob");
|
||||
let raw_redacted_event_from_bob = event_builder.make_sync_redacted_message_event_with_id(
|
||||
&BOB,
|
||||
redacted_event_id_from_bob,
|
||||
RedactedReactionEventContent::new(),
|
||||
);
|
||||
sync_builder.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(raw_redacted_event_from_bob.clone()),
|
||||
);
|
||||
|
||||
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
|
||||
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
|
||||
server.reset().await;
|
||||
|
||||
// Clear the timeline to make sure the old item does not need to be
|
||||
// available in it for the reply to work.
|
||||
timeline.clear().await;
|
||||
assert_next_matches!(timeline_stream, VectorDiff::Clear);
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
|
||||
// Now, let's reply to a message sent by `BOB`.
|
||||
Mock::given(method("PUT"))
|
||||
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*"))
|
||||
.respond_with(move |req: &Request| {
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
let reply_event = req
|
||||
.body_json::<RoomMessageEventContent>()
|
||||
.expect("Failed to deserialize the event");
|
||||
|
||||
assert_matches!(reply_event.relates_to, Some(Relation::Reply { in_reply_to: InReplyTo { event_id, .. } }) => {
|
||||
assert_eq!(event_id, redacted_event_id_from_bob);
|
||||
});
|
||||
assert_matches!(reply_event.mentions, Some(Mentions { user_ids, room: false, .. }) => {
|
||||
assert_eq!(user_ids.len(), 1);
|
||||
assert!(user_ids.contains(*BOB));
|
||||
});
|
||||
|
||||
ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$reply_event" }))
|
||||
})
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Since we assume we can't use the timeline item directly in this use case, the
|
||||
// API will fetch the event from the server directly so we need to mock the
|
||||
// response.
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(raw_redacted_event_from_bob.json()))
|
||||
.expect(1)
|
||||
.named("event_1")
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let replied_to_info =
|
||||
timeline.replied_to_info_from_event_id(redacted_event_id_from_bob).await.unwrap();
|
||||
timeline
|
||||
.send_reply(
|
||||
RoomMessageEventContentWithoutRelation::text_plain("Replying to Bob"),
|
||||
replied_to_info,
|
||||
ForwardThread::Yes,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Let the sending queue handle the event.
|
||||
yield_now().await;
|
||||
|
||||
let reply_item = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value);
|
||||
|
||||
assert_matches!(reply_item.send_state(), Some(EventSendState::NotSentYet));
|
||||
let reply_message = reply_item.content().as_message().unwrap();
|
||||
assert_eq!(reply_message.body(), "Replying to Bob");
|
||||
let in_reply_to = reply_message.in_reply_to().unwrap();
|
||||
assert_eq!(in_reply_to.event_id, redacted_event_id_from_bob);
|
||||
|
||||
let diff = timeout(timeline_stream.next(), Duration::from_secs(1)).await.unwrap().unwrap();
|
||||
assert_let!(VectorDiff::Set { index: 0, value: reply_item_remote_echo } = diff);
|
||||
|
||||
assert_matches!(reply_item_remote_echo.send_state(), Some(EventSendState::Sent { .. }));
|
||||
let reply_message = reply_item_remote_echo.content().as_message().unwrap();
|
||||
assert_eq!(reply_message.body(), "Replying to Bob");
|
||||
let in_reply_to = reply_message.in_reply_to().unwrap();
|
||||
assert_eq!(in_reply_to.event_id, redacted_event_id_from_bob);
|
||||
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
@@ -139,6 +139,22 @@ impl EventBuilder {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn make_sync_redacted_message_event_with_id<C: RedactedMessageLikeEventContent>(
|
||||
&self,
|
||||
sender: &UserId,
|
||||
event_id: &EventId,
|
||||
content: C,
|
||||
) -> Raw<AnySyncTimelineEvent> {
|
||||
sync_timeline_event!({
|
||||
"type": content.event_type(),
|
||||
"content": content,
|
||||
"event_id": event_id,
|
||||
"sender": sender,
|
||||
"origin_server_ts": self.next_server_ts(),
|
||||
"unsigned": self.make_redacted_unsigned(sender),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn make_sync_state_event<C: StateEventContent>(
|
||||
&self,
|
||||
sender: &UserId,
|
||||
|
||||
Reference in New Issue
Block a user