mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-03 05:25:24 -04:00
refactor(timeline): push the reply logic down into matrix_sdk (#4842)
This achieves step 2 of #4835. Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
This commit is contained in:
@@ -27,7 +27,7 @@ use matrix_sdk::{
|
||||
},
|
||||
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
|
||||
event_cache::RoomPaginationStatus,
|
||||
room::edit::EditedContent as SdkEditedContent,
|
||||
room::{edit::EditedContent as SdkEditedContent, reply::EnforceThread},
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
self, EventItemOrigin, Profile, RepliedToEvent, TimelineDetails,
|
||||
@@ -473,14 +473,8 @@ impl Timeline {
|
||||
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(), replied_to_info, timeline::EnforceThread::MaybeThreaded)
|
||||
.send_reply((*msg).clone(), event_id, EnforceThread::MaybeThreaded)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
Ok(())
|
||||
@@ -505,17 +499,11 @@ impl Timeline {
|
||||
is_reply: bool,
|
||||
) -> 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(),
|
||||
replied_to_info,
|
||||
timeline::EnforceThread::Threaded(if is_reply {
|
||||
event_id,
|
||||
EnforceThread::Threaded(if is_reply {
|
||||
ReplyWithinThread::Yes
|
||||
} else {
|
||||
ReplyWithinThread::No
|
||||
|
||||
@@ -12,6 +12,11 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
- [**breaking**] Optionally allow starting threads with `Timeline::send_reply`.
|
||||
([4819](https://github.com/matrix-org/matrix-rust-sdk/pull/4819))
|
||||
- [**breaking**] Push `RepliedToInfo`, `ReplyContent`, `EnforceThread` and
|
||||
`UnsupportedReplyItem` (becoming `ReplyError`) down into matrix_sdk.
|
||||
[`Timeline::send_reply()`] now takes an event ID rather than a `RepliedToInfo`.
|
||||
`Timeline::replied_to_info_from_event_id` has been made private in `matrix_sdk`.
|
||||
([4842](https://github.com/matrix-org/matrix-rust-sdk/pull/4842))
|
||||
|
||||
### Refactor
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use matrix_sdk::{
|
||||
event_cache::{paginator::PaginatorError, EventCacheError},
|
||||
room::reply::ReplyError,
|
||||
send_queue::RoomSendQueueError,
|
||||
HttpError,
|
||||
};
|
||||
@@ -73,6 +74,10 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
EditError(#[from] EditError),
|
||||
|
||||
/// An error happened while attempting to reply to an event.
|
||||
#[error(transparent)]
|
||||
ReplyError(#[from] ReplyError),
|
||||
|
||||
/// An error happened while attempting to redact an event.
|
||||
#[error(transparent)]
|
||||
RedactError(#[from] RedactError),
|
||||
@@ -119,20 +124,6 @@ pub enum PaginationError {
|
||||
Paginator(#[source] PaginatorError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UnsupportedReplyItem {
|
||||
#[error("local messages whose event ID is not known can't be replied to currently")]
|
||||
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(Debug, Error)]
|
||||
pub enum UnsupportedEditItem {
|
||||
#[error("tried to edit a non-poll event")]
|
||||
|
||||
@@ -28,7 +28,7 @@ use matrix_sdk::{
|
||||
event_cache::{EventCacheDropHandles, RoomEventCache},
|
||||
event_handler::EventHandlerHandle,
|
||||
executor::JoinHandle,
|
||||
room::{edit::EditedContent, Receipts, Room},
|
||||
room::{edit::EditedContent, reply::EnforceThread, Receipts, Room},
|
||||
send_queue::{RoomSendQueueError, SendHandle},
|
||||
Client, Result,
|
||||
};
|
||||
@@ -39,25 +39,17 @@ use ruma::{
|
||||
events::{
|
||||
poll::unstable_start::{NewUnstablePollStartEventContent, UnstablePollStartEventContent},
|
||||
receipt::{Receipt, ReceiptThread},
|
||||
relation::Thread,
|
||||
room::{
|
||||
encrypted::Relation as EncryptedRelation,
|
||||
message::{
|
||||
AddMentions, ForwardThread, OriginalRoomMessageEvent, Relation, ReplyWithinThread,
|
||||
RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
message::RoomMessageEventContentWithoutRelation,
|
||||
pinned_events::RoomPinnedEventsEventContent,
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
SyncMessageLikeEvent,
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomVersionId, UserId,
|
||||
EventId, OwnedEventId, RoomVersionId, UserId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use subscriber::TimelineWithDropHandle;
|
||||
use thiserror::Error;
|
||||
use tracing::{error, instrument, trace, warn};
|
||||
use tracing::{instrument, trace, warn};
|
||||
|
||||
use self::{
|
||||
algorithms::rfind_event_by_id, controller::TimelineController, futures::SendAttachment,
|
||||
@@ -100,57 +92,6 @@ pub use self::{
|
||||
virtual_item::VirtualTimelineItem,
|
||||
};
|
||||
|
||||
/// Information needed to reply to an event.
|
||||
#[derive(Debug, Clone)]
|
||||
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,
|
||||
}
|
||||
|
||||
impl RepliedToInfo {
|
||||
/// The event ID of the event to reply to.
|
||||
pub fn event_id(&self) -> &EventId {
|
||||
&self.event_id
|
||||
}
|
||||
|
||||
/// The sender of the event to reply to.
|
||||
pub fn sender(&self) -> &UserId {
|
||||
&self.sender
|
||||
}
|
||||
}
|
||||
|
||||
/// The content of a reply.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ReplyContent {
|
||||
/// Content of a message event.
|
||||
Message(RoomMessageEventContent),
|
||||
/// Content of any other kind of event stored as raw JSON.
|
||||
Raw(Raw<AnySyncTimelineEvent>),
|
||||
}
|
||||
|
||||
/// Whether or not to enforce a [`Relation::Thread`] when sending a reply.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[allow(clippy::exhaustive_enums)]
|
||||
pub enum EnforceThread {
|
||||
/// A thread relation is enforced. If the original message does not have a
|
||||
/// thread relation itself, a new thread is started.
|
||||
Threaded(ReplyWithinThread),
|
||||
|
||||
/// A thread relation is not enforced. If the original message has a thread
|
||||
/// relation, it is forwarded.
|
||||
MaybeThreaded,
|
||||
|
||||
/// A thread relation is not enforced. If the original message has a thread
|
||||
/// relation, it is *not* forwarded.
|
||||
Unthreaded,
|
||||
}
|
||||
|
||||
/// A high-level view into a regular¹ room's contents.
|
||||
///
|
||||
/// ¹ This type is meant to be used in the context of rooms without a
|
||||
@@ -331,162 +272,21 @@ impl Timeline {
|
||||
///
|
||||
/// * `content` - The content of the reply
|
||||
///
|
||||
/// * `replied_to_info` - A wrapper that contains the event ID, sender,
|
||||
/// content and timestamp of the event to reply to
|
||||
/// * `event_id` - The ID of the event to reply to
|
||||
///
|
||||
/// * `enforce_thread` - Whether to enforce a thread relation on the reply
|
||||
#[instrument(skip(self, content, replied_to_info))]
|
||||
#[instrument(skip(self, content))]
|
||||
pub async fn send_reply(
|
||||
&self,
|
||||
content: RoomMessageEventContentWithoutRelation,
|
||||
replied_to_info: RepliedToInfo,
|
||||
event_id: OwnedEventId,
|
||||
enforce_thread: EnforceThread,
|
||||
) -> Result<(), RoomSendQueueError> {
|
||||
// [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 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() == replied_to_info.sender {
|
||||
AddMentions::No
|
||||
} else {
|
||||
AddMentions::Yes
|
||||
};
|
||||
|
||||
let content = match replied_to_info.content {
|
||||
ReplyContent::Message(replied_to_content) => {
|
||||
let event = OriginalRoomMessageEvent {
|
||||
event_id: replied_to_info.event_id,
|
||||
sender: replied_to_info.sender,
|
||||
origin_server_ts: replied_to_info.timestamp,
|
||||
room_id: self.room().room_id().to_owned(),
|
||||
content: replied_to_content,
|
||||
unsigned: Default::default(),
|
||||
};
|
||||
|
||||
match enforce_thread {
|
||||
EnforceThread::Threaded(is_reply) => {
|
||||
content.make_for_thread(&event, is_reply, mention_the_sender)
|
||||
}
|
||||
EnforceThread::MaybeThreaded => {
|
||||
content.make_reply_to(&event, ForwardThread::Yes, mention_the_sender)
|
||||
}
|
||||
EnforceThread::Unthreaded => {
|
||||
content.make_reply_to(&event, ForwardThread::No, mention_the_sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReplyContent::Raw(raw_event) => {
|
||||
match enforce_thread {
|
||||
EnforceThread::Threaded(is_reply) => {
|
||||
// Some of the code below technically belongs into ruma. However,
|
||||
// reply fallbacks have been removed in Matrix 1.13 which means
|
||||
// both match arms can use the successor of make_for_thread in
|
||||
// the next ruma release.
|
||||
#[derive(Deserialize)]
|
||||
struct ContentDeHelper {
|
||||
#[serde(rename = "m.relates_to")]
|
||||
relates_to: Option<EncryptedRelation>,
|
||||
}
|
||||
|
||||
let previous_content =
|
||||
raw_event.get_field::<ContentDeHelper>("content").ok().flatten();
|
||||
|
||||
let mut content = if is_reply == ReplyWithinThread::Yes {
|
||||
content.make_reply_to_raw(
|
||||
&raw_event,
|
||||
replied_to_info.event_id.to_owned(),
|
||||
self.room().room_id(),
|
||||
ForwardThread::No,
|
||||
mention_the_sender,
|
||||
)
|
||||
} else {
|
||||
content.into()
|
||||
};
|
||||
|
||||
let thread_root = if let Some(EncryptedRelation::Thread(thread)) =
|
||||
previous_content.as_ref().and_then(|c| c.relates_to.as_ref())
|
||||
{
|
||||
thread.event_id.to_owned()
|
||||
} else {
|
||||
replied_to_info.event_id.to_owned()
|
||||
};
|
||||
|
||||
let thread = if is_reply == ReplyWithinThread::Yes {
|
||||
Thread::reply(thread_root, replied_to_info.event_id)
|
||||
} else {
|
||||
Thread::plain(thread_root, replied_to_info.event_id)
|
||||
};
|
||||
|
||||
content.relates_to = Some(Relation::Thread(thread));
|
||||
content
|
||||
}
|
||||
|
||||
EnforceThread::MaybeThreaded => content.make_reply_to_raw(
|
||||
&raw_event,
|
||||
replied_to_info.event_id,
|
||||
self.room().room_id(),
|
||||
ForwardThread::Yes,
|
||||
mention_the_sender,
|
||||
),
|
||||
|
||||
EnforceThread::Unthreaded => content.make_reply_to_raw(
|
||||
&raw_event,
|
||||
replied_to_info.event_id,
|
||||
self.room().room_id(),
|
||||
ForwardThread::No,
|
||||
mention_the_sender,
|
||||
),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.send(content.into()).await?;
|
||||
|
||||
) -> Result<(), Error> {
|
||||
let content = self.room().make_reply_event(content, &event_id, enforce_thread).await?;
|
||||
self.send(content).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the information needed to reply to the event with the given ID.
|
||||
pub async fn replied_to_info_from_event_id(
|
||||
&self,
|
||||
event_id: &EventId,
|
||||
) -> Result<RepliedToInfo, UnsupportedReplyItem> {
|
||||
let event = self.room().load_or_fetch_event(event_id, None).await.map_err(|error| {
|
||||
error!("Failed to fetch event with ID {event_id} with error: {error}");
|
||||
UnsupportedReplyItem::MissingEvent
|
||||
})?;
|
||||
|
||||
let raw_event = event.into_raw();
|
||||
let event = raw_event.deserialize().map_err(|error| {
|
||||
error!("Failed to deserialize event with ID {event_id} with error: {error}");
|
||||
UnsupportedReplyItem::FailedToDeserializeEvent
|
||||
})?;
|
||||
|
||||
let reply_content = match &event {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
if let AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(
|
||||
original_event,
|
||||
)) = event
|
||||
{
|
||||
ReplyContent::Message(original_event.content.clone())
|
||||
} else {
|
||||
ReplyContent::Raw(raw_event)
|
||||
}
|
||||
}
|
||||
AnySyncTimelineEvent::State(_) => return Err(UnsupportedReplyItem::StateEvent),
|
||||
};
|
||||
|
||||
Ok(RepliedToInfo {
|
||||
event_id: event_id.to_owned(),
|
||||
sender: event.sender().to_owned(),
|
||||
timestamp: event.origin_server_ts(),
|
||||
content: reply_content,
|
||||
})
|
||||
}
|
||||
|
||||
/// Edit an event given its [`TimelineEventItemId`] and some new content.
|
||||
///
|
||||
/// Only supports events for which [`EventTimelineItem::is_editable()`]
|
||||
|
||||
@@ -4,14 +4,14 @@ use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk::{room::reply::EnforceThread, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_base::timeout::timeout;
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, JoinedRoomBuilder, ALICE, BOB, CAROL,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
AggregatedTimelineItemContent, AggregatedTimelineItemContentKind, EnforceThread,
|
||||
Error as TimelineError, EventSendState, RoomExt, TimelineDetails, TimelineItemContent,
|
||||
AggregatedTimelineItemContent, AggregatedTimelineItemContentKind, Error as TimelineError,
|
||||
EventSendState, RoomExt, TimelineDetails, TimelineItemContent,
|
||||
};
|
||||
use ruma::{
|
||||
event_id,
|
||||
@@ -719,11 +719,10 @@ async fn test_send_reply() {
|
||||
.mount()
|
||||
.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,
|
||||
event_id_from_bob.to_owned(),
|
||||
EnforceThread::MaybeThreaded,
|
||||
)
|
||||
.await
|
||||
@@ -820,11 +819,10 @@ async fn test_send_reply_to_self() {
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
let replied_to_info = timeline.replied_to_info_from_event_id(event_id_from_self).await.unwrap();
|
||||
timeline
|
||||
.send_reply(
|
||||
RoomMessageEventContentWithoutRelation::text_plain("Replying to self"),
|
||||
replied_to_info,
|
||||
event_id_from_self.to_owned(),
|
||||
EnforceThread::MaybeThreaded,
|
||||
)
|
||||
.await
|
||||
@@ -887,11 +885,10 @@ async fn test_send_reply_to_threaded() {
|
||||
|
||||
server.mock_room_send().ok(event_id!("$reply_event")).mock_once().mount().await;
|
||||
|
||||
let replied_to_info = timeline.replied_to_info_from_event_id(event_id_1).await.unwrap();
|
||||
timeline
|
||||
.send_reply(
|
||||
RoomMessageEventContentWithoutRelation::text_plain("Hello, Bob!"),
|
||||
replied_to_info,
|
||||
event_id_1.to_owned(),
|
||||
EnforceThread::MaybeThreaded,
|
||||
)
|
||||
.await
|
||||
@@ -991,11 +988,10 @@ async fn test_send_reply_with_event_id() {
|
||||
.mount()
|
||||
.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,
|
||||
event_id_from_bob.to_owned(),
|
||||
EnforceThread::MaybeThreaded,
|
||||
)
|
||||
.await
|
||||
@@ -1078,11 +1074,10 @@ async fn test_send_reply_enforce_thread() {
|
||||
.mount()
|
||||
.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,
|
||||
event_id_from_bob.to_owned(),
|
||||
EnforceThread::Threaded(ReplyWithinThread::No),
|
||||
)
|
||||
.await
|
||||
@@ -1176,11 +1171,10 @@ async fn test_send_reply_enforce_thread_is_reply() {
|
||||
.mount()
|
||||
.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,
|
||||
event_id_from_bob.to_owned(),
|
||||
EnforceThread::Threaded(ReplyWithinThread::Yes),
|
||||
)
|
||||
.await
|
||||
@@ -1270,12 +1264,10 @@ async fn test_send_reply_with_event_id_that_is_redacted() {
|
||||
.mount()
|
||||
.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,
|
||||
redacted_event_id_from_bob.to_owned(),
|
||||
EnforceThread::MaybeThreaded,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
|
||||
//! Facilities to edit existing events.
|
||||
|
||||
use std::future::Future;
|
||||
|
||||
use matrix_sdk_base::{deserialized_responses::TimelineEvent, SendOutsideWasm};
|
||||
use ruma::{
|
||||
events::{
|
||||
poll::unstable_start::{
|
||||
@@ -36,6 +33,7 @@ use ruma::{
|
||||
use thiserror::Error;
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use super::EventSource;
|
||||
use crate::Room;
|
||||
|
||||
/// The new content that will replace the previous event's content.
|
||||
@@ -125,21 +123,6 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
trait EventSource {
|
||||
fn get_event(
|
||||
&self,
|
||||
event_id: &EventId,
|
||||
) -> impl Future<Output = Result<TimelineEvent, EditError>> + SendOutsideWasm;
|
||||
}
|
||||
|
||||
impl EventSource for &Room {
|
||||
async fn get_event(&self, event_id: &EventId) -> Result<TimelineEvent, EditError> {
|
||||
self.load_or_fetch_event(event_id, None)
|
||||
.await
|
||||
.map_err(|err| EditError::Fetch(Box::new(err)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn make_edit_event<S: EventSource>(
|
||||
source: S,
|
||||
room_id: &RoomId,
|
||||
@@ -147,7 +130,7 @@ async fn make_edit_event<S: EventSource>(
|
||||
event_id: &EventId,
|
||||
new_content: EditedContent,
|
||||
) -> Result<AnyMessageLikeEventContent, EditError> {
|
||||
let target = source.get_event(event_id).await?;
|
||||
let target = source.get_event(event_id).await.map_err(|err| EditError::Fetch(Box::new(err)))?;
|
||||
|
||||
let event = target.raw().deserialize().map_err(EditError::Deserialize)?;
|
||||
|
||||
@@ -352,14 +335,11 @@ mod tests {
|
||||
room::message::{MessageType, Relation, RoomMessageEventContentWithoutRelation},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent, Mentions,
|
||||
},
|
||||
owned_mxc_uri, owned_user_id, room_id,
|
||||
serde::Raw,
|
||||
user_id, EventId, OwnedEventId,
|
||||
owned_mxc_uri, owned_user_id, room_id, user_id, EventId, OwnedEventId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{make_edit_event, EditError, EventSource};
|
||||
use crate::room::edit::EditedContent;
|
||||
use crate::{room::edit::EditedContent, Error};
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestEventCache {
|
||||
@@ -367,7 +347,7 @@ mod tests {
|
||||
}
|
||||
|
||||
impl EventSource for TestEventCache {
|
||||
async fn get_event(&self, event_id: &EventId) -> Result<TimelineEvent, EditError> {
|
||||
async fn get_event(&self, event_id: &EventId) -> Result<TimelineEvent, Error> {
|
||||
Ok(self.events.get(event_id).unwrap().clone())
|
||||
}
|
||||
}
|
||||
@@ -378,26 +358,10 @@ mod tests {
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
|
||||
let f = EventFactory::new();
|
||||
cache.events.insert(
|
||||
event_id.to_owned(),
|
||||
// TODO: use the EventFactory for state events too.
|
||||
TimelineEvent::new(
|
||||
Raw::<AnySyncTimelineEvent>::from_json_string(
|
||||
json!({
|
||||
"content": {
|
||||
"name": "The room name"
|
||||
},
|
||||
"event_id": event_id,
|
||||
"sender": own_user_id,
|
||||
"state_key": "",
|
||||
"origin_server_ts": 1,
|
||||
"type": "m.room.name",
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
f.room_name("The room name").event_id(event_id).sender(own_user_id).into(),
|
||||
);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::{BTreeMap, HashMap},
|
||||
future::Future,
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
@@ -46,8 +47,8 @@ use matrix_sdk_base::{
|
||||
event_cache::store::media::IgnoreMediaRetentionPolicy,
|
||||
media::MediaThumbnailSettings,
|
||||
store::StateStoreExt,
|
||||
ComposerDraft, EncryptionState, RoomInfoNotableUpdateReasons, RoomMemberships, StateChanges,
|
||||
StateStoreDataKey, StateStoreDataValue,
|
||||
ComposerDraft, EncryptionState, RoomInfoNotableUpdateReasons, RoomMemberships, SendOutsideWasm,
|
||||
StateChanges, StateStoreDataKey, StateStoreDataValue,
|
||||
};
|
||||
#[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))]
|
||||
use matrix_sdk_common::BoxFuture;
|
||||
@@ -166,6 +167,7 @@ pub mod knock_requests;
|
||||
mod member;
|
||||
mod messages;
|
||||
pub mod power_levels;
|
||||
pub mod reply;
|
||||
|
||||
/// Contains all the functionality for modifying the privacy settings in a room.
|
||||
pub mod privacy_settings;
|
||||
@@ -3773,6 +3775,19 @@ impl TryFrom<Int> for ReportedContentScore {
|
||||
}
|
||||
}
|
||||
|
||||
trait EventSource {
|
||||
fn get_event(
|
||||
&self,
|
||||
event_id: &EventId,
|
||||
) -> impl Future<Output = Result<TimelineEvent, Error>> + SendOutsideWasm;
|
||||
}
|
||||
|
||||
impl EventSource for &Room {
|
||||
async fn get_event(&self, event_id: &EventId) -> Result<TimelineEvent, Error> {
|
||||
self.load_or_fetch_event(event_id, None).await
|
||||
}
|
||||
}
|
||||
|
||||
/// The error type returned when a checked `ReportedContentScore` conversion
|
||||
/// fails.
|
||||
#[derive(Debug, Clone, Error)]
|
||||
|
||||
590
crates/matrix-sdk/src/room/reply.rs
Normal file
590
crates/matrix-sdk/src/room/reply.rs
Normal file
@@ -0,0 +1,590 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Facilities to reply to existing events.
|
||||
|
||||
use ruma::{
|
||||
events::{
|
||||
relation::Thread,
|
||||
room::{
|
||||
encrypted::Relation as EncryptedRelation,
|
||||
message::{
|
||||
AddMentions, ForwardThread, OriginalRoomMessageEvent, Relation, ReplyWithinThread,
|
||||
RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
SyncMessageLikeEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use tracing::{error, instrument};
|
||||
|
||||
use super::{EventSource, Room};
|
||||
|
||||
/// Information needed to reply to an event.
|
||||
#[derive(Debug, Clone)]
|
||||
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, Clone)]
|
||||
enum ReplyContent {
|
||||
/// Content of a message event.
|
||||
Message(RoomMessageEventContent),
|
||||
/// Content of any other kind of event stored as raw JSON.
|
||||
Raw(Raw<AnySyncTimelineEvent>),
|
||||
}
|
||||
|
||||
/// Errors specific to unsupported replies.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ReplyError {
|
||||
/// We couldn't fetch the remote event with /room/event.
|
||||
#[error("Couldn't fetch the remote event: {0}")]
|
||||
Fetch(Box<crate::Error>),
|
||||
/// The event to reply to could not be deserialized.
|
||||
#[error("failed to deserialize event to reply to")]
|
||||
Deserialization,
|
||||
/// State events cannot be replied to.
|
||||
#[error("tried to reply to a state event")]
|
||||
StateEvent,
|
||||
}
|
||||
|
||||
/// Whether or not to enforce a [`Relation::Thread`] when sending a reply.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EnforceThread {
|
||||
/// A thread relation is enforced. If the original message does not have a
|
||||
/// thread relation itself, a new thread is started.
|
||||
Threaded(ReplyWithinThread),
|
||||
|
||||
/// A thread relation is not enforced. If the original message has a thread
|
||||
/// relation, it is forwarded.
|
||||
MaybeThreaded,
|
||||
|
||||
/// A thread relation is not enforced. If the original message has a thread
|
||||
/// relation, it is *not* forwarded.
|
||||
Unthreaded,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
/// Create a new reply event for the target event id with the specified
|
||||
/// content.
|
||||
///
|
||||
/// The event can then be sent with [`Room::send`] or a
|
||||
/// [`crate::send_queue::RoomSendQueue`].
|
||||
#[instrument(skip(self, content), fields(room = %self.room_id()))]
|
||||
pub async fn make_reply_event(
|
||||
&self,
|
||||
content: RoomMessageEventContentWithoutRelation,
|
||||
event_id: &EventId,
|
||||
enforce_thread: EnforceThread,
|
||||
) -> Result<AnyMessageLikeEventContent, ReplyError> {
|
||||
make_reply_event(
|
||||
self,
|
||||
self.room_id(),
|
||||
self.own_user_id(),
|
||||
content,
|
||||
event_id,
|
||||
enforce_thread,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn make_reply_event<S: EventSource>(
|
||||
source: S,
|
||||
room_id: &RoomId,
|
||||
own_user_id: &UserId,
|
||||
content: RoomMessageEventContentWithoutRelation,
|
||||
event_id: &EventId,
|
||||
enforce_thread: EnforceThread,
|
||||
) -> Result<AnyMessageLikeEventContent, ReplyError> {
|
||||
let replied_to_info = replied_to_info_from_event_id(source, event_id).await?;
|
||||
|
||||
// [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 the replied to event has been written by the current user, let's toggle to
|
||||
// `AddMentions::No`.
|
||||
let mention_the_sender =
|
||||
if own_user_id == replied_to_info.sender { AddMentions::No } else { AddMentions::Yes };
|
||||
|
||||
let content = match replied_to_info.content {
|
||||
ReplyContent::Message(replied_to_content) => {
|
||||
let event = OriginalRoomMessageEvent {
|
||||
event_id: replied_to_info.event_id,
|
||||
sender: replied_to_info.sender,
|
||||
origin_server_ts: replied_to_info.timestamp,
|
||||
room_id: room_id.to_owned(),
|
||||
content: replied_to_content,
|
||||
unsigned: Default::default(),
|
||||
};
|
||||
|
||||
match enforce_thread {
|
||||
EnforceThread::Threaded(is_reply) => {
|
||||
content.make_for_thread(&event, is_reply, mention_the_sender)
|
||||
}
|
||||
EnforceThread::MaybeThreaded => {
|
||||
content.make_reply_to(&event, ForwardThread::Yes, mention_the_sender)
|
||||
}
|
||||
EnforceThread::Unthreaded => {
|
||||
content.make_reply_to(&event, ForwardThread::No, mention_the_sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReplyContent::Raw(raw_event) => {
|
||||
match enforce_thread {
|
||||
EnforceThread::Threaded(is_reply) => {
|
||||
// Some of the code below technically belongs into ruma. However,
|
||||
// reply fallbacks have been removed in Matrix 1.13 which means
|
||||
// both match arms can use the successor of make_for_thread in
|
||||
// the next ruma release.
|
||||
#[derive(Deserialize)]
|
||||
struct ContentDeHelper {
|
||||
#[serde(rename = "m.relates_to")]
|
||||
relates_to: Option<EncryptedRelation>,
|
||||
}
|
||||
|
||||
let previous_content =
|
||||
raw_event.get_field::<ContentDeHelper>("content").ok().flatten();
|
||||
|
||||
let mut content = if is_reply == ReplyWithinThread::Yes {
|
||||
content.make_reply_to_raw(
|
||||
&raw_event,
|
||||
replied_to_info.event_id.to_owned(),
|
||||
room_id,
|
||||
ForwardThread::No,
|
||||
mention_the_sender,
|
||||
)
|
||||
} else {
|
||||
content.into()
|
||||
};
|
||||
|
||||
let thread_root = if let Some(EncryptedRelation::Thread(thread)) =
|
||||
previous_content.as_ref().and_then(|c| c.relates_to.as_ref())
|
||||
{
|
||||
thread.event_id.to_owned()
|
||||
} else {
|
||||
replied_to_info.event_id.to_owned()
|
||||
};
|
||||
|
||||
let thread = if is_reply == ReplyWithinThread::Yes {
|
||||
Thread::reply(thread_root, replied_to_info.event_id)
|
||||
} else {
|
||||
Thread::plain(thread_root, replied_to_info.event_id)
|
||||
};
|
||||
|
||||
content.relates_to = Some(Relation::Thread(thread));
|
||||
content
|
||||
}
|
||||
|
||||
EnforceThread::MaybeThreaded => content.make_reply_to_raw(
|
||||
&raw_event,
|
||||
replied_to_info.event_id,
|
||||
room_id,
|
||||
ForwardThread::Yes,
|
||||
mention_the_sender,
|
||||
),
|
||||
|
||||
EnforceThread::Unthreaded => content.make_reply_to_raw(
|
||||
&raw_event,
|
||||
replied_to_info.event_id,
|
||||
room_id,
|
||||
ForwardThread::No,
|
||||
mention_the_sender,
|
||||
),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(content.into())
|
||||
}
|
||||
|
||||
async fn replied_to_info_from_event_id<S: EventSource>(
|
||||
source: S,
|
||||
event_id: &EventId,
|
||||
) -> Result<RepliedToInfo, ReplyError> {
|
||||
let event = source.get_event(event_id).await.map_err(|err| ReplyError::Fetch(Box::new(err)))?;
|
||||
|
||||
let raw_event = event.into_raw();
|
||||
let event = raw_event.deserialize().map_err(|_| ReplyError::Deserialization)?;
|
||||
|
||||
let reply_content = match &event {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
if let AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(
|
||||
original_event,
|
||||
)) = event
|
||||
{
|
||||
ReplyContent::Message(original_event.content.clone())
|
||||
} else {
|
||||
ReplyContent::Raw(raw_event)
|
||||
}
|
||||
}
|
||||
AnySyncTimelineEvent::State(_) => return Err(ReplyError::StateEvent),
|
||||
};
|
||||
|
||||
Ok(RepliedToInfo {
|
||||
event_id: event_id.to_owned(),
|
||||
sender: event.sender().to_owned(),
|
||||
timestamp: event.origin_server_ts(),
|
||||
content: reply_content,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use assert_matches2::{assert_let, assert_matches};
|
||||
use matrix_sdk_base::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_test::{async_test, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::{
|
||||
room::message::{Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
},
|
||||
room_id,
|
||||
serde::Raw,
|
||||
user_id, EventId, OwnedEventId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{make_reply_event, EnforceThread, EventSource, ReplyError};
|
||||
use crate::{event_cache::EventCacheError, Error};
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestEventCache {
|
||||
events: BTreeMap<OwnedEventId, TimelineEvent>,
|
||||
}
|
||||
|
||||
impl EventSource for TestEventCache {
|
||||
async fn get_event(&self, event_id: &EventId) -> Result<TimelineEvent, Error> {
|
||||
self.events
|
||||
.get(event_id)
|
||||
.cloned()
|
||||
.ok_or(Error::EventCache(EventCacheError::ClientDropped))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_cannot_reply_to_unknown_event() {
|
||||
let event_id = event_id!("$1");
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
let f = EventFactory::new();
|
||||
cache.events.insert(
|
||||
event_id.to_owned(),
|
||||
f.text_msg("hi").event_id(event_id).sender(own_user_id).into(),
|
||||
);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
|
||||
|
||||
assert_matches!(
|
||||
make_reply_event(
|
||||
cache,
|
||||
room_id,
|
||||
own_user_id,
|
||||
content,
|
||||
event_id!("$2"),
|
||||
EnforceThread::Unthreaded,
|
||||
)
|
||||
.await,
|
||||
Err(ReplyError::Fetch(_))
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_cannot_reply_to_invalid_event() {
|
||||
let event_id = event_id!("$1");
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
|
||||
cache.events.insert(
|
||||
event_id.to_owned(),
|
||||
TimelineEvent::new(
|
||||
Raw::<AnySyncTimelineEvent>::from_json_string(
|
||||
json!({
|
||||
"content": {
|
||||
"body": "hi"
|
||||
},
|
||||
"event_id": event_id,
|
||||
"origin_server_ts": 1,
|
||||
"type": "m.room.message",
|
||||
// Invalid because sender is missing
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
|
||||
|
||||
assert_matches!(
|
||||
make_reply_event(
|
||||
cache,
|
||||
room_id,
|
||||
own_user_id,
|
||||
content,
|
||||
event_id,
|
||||
EnforceThread::Unthreaded,
|
||||
)
|
||||
.await,
|
||||
Err(ReplyError::Deserialization)
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_cannot_reply_to_state_event() {
|
||||
let event_id = event_id!("$1");
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
let f = EventFactory::new();
|
||||
cache.events.insert(
|
||||
event_id.to_owned(),
|
||||
f.room_name("lobby").event_id(event_id).sender(own_user_id).into(),
|
||||
);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
|
||||
|
||||
assert_matches!(
|
||||
make_reply_event(
|
||||
cache,
|
||||
room_id,
|
||||
own_user_id,
|
||||
content,
|
||||
event_id,
|
||||
EnforceThread::Unthreaded,
|
||||
)
|
||||
.await,
|
||||
Err(ReplyError::StateEvent)
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_reply_unthreaded() {
|
||||
let event_id = event_id!("$1");
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
let f = EventFactory::new();
|
||||
cache.events.insert(
|
||||
event_id.to_owned(),
|
||||
f.text_msg("hi").event_id(event_id).sender(own_user_id).into(),
|
||||
);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
|
||||
|
||||
let reply_event = make_reply_event(
|
||||
cache,
|
||||
room_id,
|
||||
own_user_id,
|
||||
content,
|
||||
event_id,
|
||||
EnforceThread::Unthreaded,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &reply_event);
|
||||
assert_let!(Some(Relation::Reply { in_reply_to }) = &msg.relates_to);
|
||||
|
||||
assert_eq!(in_reply_to.event_id, event_id);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_start_thread() {
|
||||
let event_id = event_id!("$1");
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
let f = EventFactory::new();
|
||||
cache.events.insert(
|
||||
event_id.to_owned(),
|
||||
f.text_msg("hi").event_id(event_id).sender(own_user_id).into(),
|
||||
);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
|
||||
|
||||
let reply_event = make_reply_event(
|
||||
cache,
|
||||
room_id,
|
||||
own_user_id,
|
||||
content,
|
||||
event_id,
|
||||
EnforceThread::Threaded(ReplyWithinThread::No),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &reply_event);
|
||||
assert_let!(Some(Relation::Thread(thread)) = &msg.relates_to);
|
||||
|
||||
assert_eq!(thread.event_id, event_id);
|
||||
assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id);
|
||||
assert!(thread.is_falling_back);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_reply_on_thread() {
|
||||
let thread_root = event_id!("$1");
|
||||
let event_id = event_id!("$2");
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
let f = EventFactory::new();
|
||||
cache.events.insert(
|
||||
thread_root.to_owned(),
|
||||
f.text_msg("hi").event_id(thread_root).sender(own_user_id).into(),
|
||||
);
|
||||
cache.events.insert(
|
||||
event_id.to_owned(),
|
||||
f.text_msg("ho")
|
||||
.in_thread(thread_root, thread_root)
|
||||
.event_id(event_id)
|
||||
.sender(own_user_id)
|
||||
.into(),
|
||||
);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
|
||||
|
||||
let reply_event = make_reply_event(
|
||||
cache,
|
||||
room_id,
|
||||
own_user_id,
|
||||
content,
|
||||
event_id,
|
||||
EnforceThread::Threaded(ReplyWithinThread::No),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &reply_event);
|
||||
assert_let!(Some(Relation::Thread(thread)) = &msg.relates_to);
|
||||
|
||||
assert_eq!(thread.event_id, thread_root);
|
||||
assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id);
|
||||
assert!(thread.is_falling_back);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_reply_on_thread_as_reply() {
|
||||
let thread_root = event_id!("$1");
|
||||
let event_id = event_id!("$2");
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
let f = EventFactory::new();
|
||||
cache.events.insert(
|
||||
thread_root.to_owned(),
|
||||
f.text_msg("hi").event_id(thread_root).sender(own_user_id).into(),
|
||||
);
|
||||
cache.events.insert(
|
||||
event_id.to_owned(),
|
||||
f.text_msg("ho")
|
||||
.in_thread(thread_root, thread_root)
|
||||
.event_id(event_id)
|
||||
.sender(own_user_id)
|
||||
.into(),
|
||||
);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
|
||||
|
||||
let reply_event = make_reply_event(
|
||||
cache,
|
||||
room_id,
|
||||
own_user_id,
|
||||
content,
|
||||
event_id,
|
||||
EnforceThread::Threaded(ReplyWithinThread::Yes),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &reply_event);
|
||||
assert_let!(Some(Relation::Thread(thread)) = &msg.relates_to);
|
||||
|
||||
assert_eq!(thread.event_id, thread_root);
|
||||
assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id);
|
||||
assert!(!thread.is_falling_back);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_reply_forwarding_thread() {
|
||||
let thread_root = event_id!("$1");
|
||||
let event_id = event_id!("$2");
|
||||
let own_user_id = user_id!("@me:saucisse.bzh");
|
||||
|
||||
let mut cache = TestEventCache::default();
|
||||
let f = EventFactory::new();
|
||||
cache.events.insert(
|
||||
thread_root.to_owned(),
|
||||
f.text_msg("hi").event_id(thread_root).sender(own_user_id).into(),
|
||||
);
|
||||
cache.events.insert(
|
||||
event_id.to_owned(),
|
||||
f.text_msg("ho")
|
||||
.in_thread(thread_root, thread_root)
|
||||
.event_id(event_id)
|
||||
.sender(own_user_id)
|
||||
.into(),
|
||||
);
|
||||
|
||||
let room_id = room_id!("!galette:saucisse.bzh");
|
||||
let content = RoomMessageEventContentWithoutRelation::text_plain("the reply");
|
||||
|
||||
let reply_event = make_reply_event(
|
||||
cache,
|
||||
room_id,
|
||||
own_user_id,
|
||||
content,
|
||||
event_id,
|
||||
EnforceThread::MaybeThreaded,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &reply_event);
|
||||
assert_let!(Some(Relation::Thread(thread)) = &msg.relates_to);
|
||||
|
||||
assert_eq!(thread.event_id, thread_root);
|
||||
assert_eq!(thread.in_reply_to.as_ref().unwrap().event_id, event_id);
|
||||
assert!(thread.is_falling_back);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user