mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-06-10 17:34:20 -04:00
sdk-ui: Move the event-fetching logic for edit and redact functions to the sdk-ui crate where they can be tested, to the edit_by_id and redact_by_id functions.
Added some tests for those, based on the existing ones.
This commit is contained in:
committed by
Jorge Martin Espinosa
parent
67df36f733
commit
548c66750f
@@ -489,18 +489,7 @@ impl Timeline {
|
||||
/// Returns whether the edit did happen. It can only return false for
|
||||
/// local events that are being processed.
|
||||
pub async fn edit(&self, id: String, new_content: EditedContent) -> Result<bool, ClientError> {
|
||||
let event = if let Ok(event_id) = EventId::parse(&id) {
|
||||
self.inner.item_by_event_id(&event_id).await
|
||||
} else {
|
||||
let transaction_id: OwnedTransactionId = id.into();
|
||||
self.inner.local_item_by_transaction_id(&transaction_id).await
|
||||
};
|
||||
if let Some(event) = event {
|
||||
let new_content: SdkEditedContent = new_content.try_into()?;
|
||||
self.inner.edit(&event, new_content).await.map_err(ClientError::from)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
self.inner.edit_by_id(&(id.into()), new_content.try_into()?).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn send_location(
|
||||
@@ -604,30 +593,13 @@ impl Timeline {
|
||||
/// being sent already. If the event was a remote event, then it will be
|
||||
/// redacted by sending a redaction request to the server.
|
||||
///
|
||||
/// Returns whether the redaction did happen. It can only return false for
|
||||
/// local events that are being processed.
|
||||
/// Will return an error if the event couldn't be redacted.
|
||||
pub async fn redact_event(
|
||||
&self,
|
||||
id: String,
|
||||
reason: Option<String>,
|
||||
) -> Result<bool, ClientError> {
|
||||
let event = if let Ok(event_id) = EventId::parse(&id) {
|
||||
self.inner.item_by_event_id(&event_id).await
|
||||
} else {
|
||||
let transaction_id: OwnedTransactionId = id.into();
|
||||
self.inner.local_item_by_transaction_id(&transaction_id).await
|
||||
};
|
||||
if let Some(event) = event {
|
||||
let removed = self
|
||||
.inner
|
||||
.redact(&event, reason.as_deref())
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
|
||||
Ok(removed)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.redact_by_id(&(id.into()), reason.as_deref()).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Load the reply details for the given event id.
|
||||
|
||||
@@ -1380,9 +1380,12 @@ impl TimelineController {
|
||||
#[instrument(skip(self))]
|
||||
pub(super) async fn fetch_in_reply_to_details(&self, event_id: &EventId) -> Result<(), Error> {
|
||||
let state = self.state.write().await;
|
||||
let (index, item) =
|
||||
rfind_event_by_id(&state.items, event_id).ok_or(Error::RemoteEventNotInTimeline)?;
|
||||
let remote_item = item.as_remote().ok_or(Error::RemoteEventNotInTimeline)?.clone();
|
||||
let (index, item) = rfind_event_by_id(&state.items, event_id)
|
||||
.ok_or(Error::EventNotInTimeline(TimelineEventItemId::EventId(event_id.to_owned())))?;
|
||||
let remote_item = item
|
||||
.as_remote()
|
||||
.ok_or(Error::EventNotInTimeline(TimelineEventItemId::EventId(event_id.to_owned())))?
|
||||
.clone();
|
||||
|
||||
let TimelineItemContent::Message(message) = item.content().clone() else {
|
||||
debug!("Event is not a message");
|
||||
@@ -1418,7 +1421,7 @@ impl TimelineController {
|
||||
// changed while waiting for the request.
|
||||
let mut state = self.state.write().await;
|
||||
let (index, item) = rfind_event_by_id(&state.items, &remote_item.event_id)
|
||||
.ok_or(Error::RemoteEventNotInTimeline)?;
|
||||
.ok_or(Error::EventNotInTimeline(TimelineEventItemId::EventId(event_id.to_owned())))?;
|
||||
|
||||
// Check the state of the event again, it might have been redacted while
|
||||
// the request was in-flight.
|
||||
|
||||
@@ -18,17 +18,18 @@ use matrix_sdk::{
|
||||
send_queue::RoomSendQueueError,
|
||||
HttpError,
|
||||
};
|
||||
use ruma::OwnedTransactionId;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::timeline::pinned_events_loader::PinnedEventsLoaderError;
|
||||
use crate::timeline::{pinned_events_loader::PinnedEventsLoaderError, TimelineEventItemId};
|
||||
|
||||
/// 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 requested event is not in the timeline.
|
||||
#[error("Event not found in timeline: {0:?}")]
|
||||
EventNotInTimeline(TimelineEventItemId),
|
||||
|
||||
/// The event is currently unsupported for this use case..
|
||||
#[error("Unsupported event")]
|
||||
@@ -76,7 +77,18 @@ pub enum Error {
|
||||
|
||||
/// An error happened while attempting to redact an event.
|
||||
#[error(transparent)]
|
||||
RedactError(HttpError),
|
||||
RedactError(RedactError),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RedactError {
|
||||
/// Local event to redact wasn't found for transaction id
|
||||
#[error("Local event to redact wasn't found for transaction {0}")]
|
||||
LocalEventNotFound(OwnedTransactionId),
|
||||
|
||||
/// An error happened while attempting to redact an event.
|
||||
#[error(transparent)]
|
||||
HttpError(#[from] HttpError),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
||||
@@ -99,6 +99,22 @@ pub enum TimelineEventItemId {
|
||||
EventId(OwnedEventId),
|
||||
}
|
||||
|
||||
impl From<String> for TimelineEventItemId {
|
||||
fn from(value: String) -> Self {
|
||||
value.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TimelineEventItemId {
|
||||
fn from(value: &str) -> Self {
|
||||
if let Ok(event_id) = EventId::parse(value) {
|
||||
TimelineEventItemId::EventId(event_id)
|
||||
} else {
|
||||
TimelineEventItemId::TransactionId(value.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An handle that usually allows to perform an action on a timeline event.
|
||||
///
|
||||
/// If the item represents a remote item, then the event id is usually
|
||||
@@ -251,7 +267,7 @@ impl EventTimelineItem {
|
||||
/// Returns the transaction ID for a local echo item that has not been sent
|
||||
/// and the event ID for a local echo item that has been sent or a
|
||||
/// remote item.
|
||||
pub(crate) fn identifier(&self) -> TimelineEventItemId {
|
||||
pub fn identifier(&self) -> TimelineEventItemId {
|
||||
match &self.kind {
|
||||
EventTimelineItemKind::Local(local) => local.identifier(),
|
||||
EventTimelineItemKind::Remote(remote) => {
|
||||
@@ -719,7 +735,7 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::{EventTimelineItem, Profile};
|
||||
use crate::timeline::TimelineDetails;
|
||||
use crate::timeline::{TimelineDetails, TimelineEventItemId};
|
||||
|
||||
#[async_test]
|
||||
async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() {
|
||||
@@ -833,6 +849,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_event_id_into_timeline_event_item_id_gets_event_id() {
|
||||
let raw_id = "$123:example.com";
|
||||
let id: TimelineEventItemId = raw_id.into();
|
||||
assert_matches!(id, TimelineEventItemId::EventId(_));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_str_into_timeline_event_item_id_gets_transaction_id() {
|
||||
let raw_id = "something something";
|
||||
let id: TimelineEventItemId = raw_id.into();
|
||||
assert_matches!(id, TimelineEventItemId::TransactionId(_));
|
||||
}
|
||||
|
||||
fn member_event(
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
|
||||
@@ -230,6 +230,17 @@ impl Timeline {
|
||||
self.controller.retry_event_decryption(self.room(), None).await;
|
||||
}
|
||||
|
||||
/// Get the current timeline item for the given [`TimelineEventItemId`], if
|
||||
/// any.
|
||||
async fn event_by_timeline_id(&self, id: &TimelineEventItemId) -> Option<EventTimelineItem> {
|
||||
match id {
|
||||
TimelineEventItemId::EventId(event_id) => self.item_by_event_id(event_id).await,
|
||||
TimelineEventItemId::TransactionId(transaction_id) => {
|
||||
self.item_by_transaction_id(transaction_id).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current timeline item for the given event ID, if any.
|
||||
///
|
||||
/// Will return a remote event, *or* a local echo that has been sent but not
|
||||
@@ -455,6 +466,21 @@ impl Timeline {
|
||||
Some(found.clone())
|
||||
}
|
||||
|
||||
/// Edit an event given its [`TimelineEventItemId`] and some new content.
|
||||
///
|
||||
/// See [`Self::edit`] for more information.
|
||||
pub async fn edit_by_id(
|
||||
&self,
|
||||
id: &TimelineEventItemId,
|
||||
new_content: EditedContent,
|
||||
) -> Result<bool, Error> {
|
||||
let Some(event) = self.event_by_timeline_id(id).await else {
|
||||
return Err(Error::EventNotInTimeline(id.clone()));
|
||||
};
|
||||
|
||||
self.edit(&event, new_content).await
|
||||
}
|
||||
|
||||
/// Edit an event.
|
||||
///
|
||||
/// Only supports events for which [`EventTimelineItem::is_editable()`]
|
||||
@@ -568,6 +594,38 @@ impl Timeline {
|
||||
SendAttachment::new(self, path.into(), mime_type, config)
|
||||
}
|
||||
|
||||
/// Redact an event given its [`TimelineEventItemId`] and an optional
|
||||
/// reason.
|
||||
///
|
||||
/// See [`Self::redact`] for more info.
|
||||
pub async fn redact_by_id(
|
||||
&self,
|
||||
id: &TimelineEventItemId,
|
||||
reason: Option<&str>,
|
||||
) -> Result<(), Error> {
|
||||
match id {
|
||||
TimelineEventItemId::TransactionId(transaction_id) => {
|
||||
let Some(event) = self.item_by_transaction_id(transaction_id).await else {
|
||||
return Err(Error::RedactError(RedactError::LocalEventNotFound(
|
||||
transaction_id.to_owned(),
|
||||
)));
|
||||
};
|
||||
let TimelineItemHandle::Local(handle) = event.handle() else {
|
||||
panic!("If the item is local, this should never happen");
|
||||
};
|
||||
handle.abort().await.map_err(RoomSendQueueError::StorageError)?;
|
||||
}
|
||||
TimelineEventItemId::EventId(event_id) => {
|
||||
self.room()
|
||||
.redact(event_id, reason, None)
|
||||
.await
|
||||
.map_err(RedactError::HttpError)
|
||||
.map_err(Error::RedactError)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Redact an event.
|
||||
///
|
||||
/// # Returns
|
||||
@@ -583,28 +641,27 @@ impl Timeline {
|
||||
reason: Option<&str>,
|
||||
) -> Result<bool, Error> {
|
||||
let event_id = match event.identifier() {
|
||||
TimelineEventItemId::TransactionId(txn_id) => {
|
||||
TimelineEventItemId::TransactionId(_) => {
|
||||
// See if we have an up-to-date timeline item with that transaction id.
|
||||
if let Some(item) = self.item_by_transaction_id(&txn_id).await {
|
||||
match item.handle() {
|
||||
TimelineItemHandle::Remote(event_id) => event_id.to_owned(),
|
||||
TimelineItemHandle::Local(handle) => {
|
||||
return Ok(handle
|
||||
.abort()
|
||||
.await
|
||||
.map_err(RoomSendQueueError::StorageError)?);
|
||||
}
|
||||
match event.handle() {
|
||||
TimelineItemHandle::Remote(event_id) => event_id.to_owned(),
|
||||
TimelineItemHandle::Local(handle) => {
|
||||
return Ok(handle
|
||||
.abort()
|
||||
.await
|
||||
.map_err(RoomSendQueueError::StorageError)?);
|
||||
}
|
||||
} else {
|
||||
warn!("Couldn't find the local echo anymore, nor a matching remote echo");
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
TimelineEventItemId::EventId(event_id) => event_id,
|
||||
};
|
||||
|
||||
self.room().redact(&event_id, reason, None).await.map_err(Error::RedactError)?;
|
||||
self.room()
|
||||
.redact(&event_id, reason, None)
|
||||
.await
|
||||
.map_err(RedactError::HttpError)
|
||||
.map_err(Error::RedactError)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ use ruma::{
|
||||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use super::{Profile, TimelineBuilder};
|
||||
use super::{Profile, RedactError, TimelineBuilder};
|
||||
use crate::timeline::{self, pinned_events_loader::PinnedEventsRoom, Timeline};
|
||||
|
||||
pub trait RoomExt {
|
||||
@@ -269,6 +269,7 @@ impl RoomDataProvider for Room {
|
||||
let _ = self
|
||||
.redact(event_id, reason, transaction_id)
|
||||
.await
|
||||
.map_err(RedactError::HttpError)
|
||||
.map_err(super::Error::RedactError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ use matrix_sdk_test::{
|
||||
async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB,
|
||||
};
|
||||
use matrix_sdk_ui::{
|
||||
timeline::{EventSendState, RoomExt, TimelineDetails, TimelineItemContent},
|
||||
timeline::{
|
||||
Error, EventSendState, RoomExt, TimelineDetails, TimelineEventItemId, TimelineItemContent,
|
||||
},
|
||||
Timeline,
|
||||
};
|
||||
use ruma::{
|
||||
@@ -46,7 +48,7 @@ use ruma::{
|
||||
},
|
||||
AnyMessageLikeEventContent, AnyTimelineEvent,
|
||||
},
|
||||
room_id,
|
||||
owned_event_id, room_id,
|
||||
serde::Raw,
|
||||
OwnedRoomId,
|
||||
};
|
||||
@@ -1092,3 +1094,263 @@ async fn test_pending_poll_edit() {
|
||||
// And nothing else.
|
||||
assert!(timeline_stream.next().now_or_never().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_edit_by_event_id() {
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let (client, server) = logged_in_client_with_server().await;
|
||||
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;
|
||||
|
||||
mock_encryption_state(&server, false).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 f = EventFactory::new();
|
||||
sync_builder.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
f.text_msg("Hello, World!")
|
||||
.sender(client.user_id().unwrap())
|
||||
.event_id(event_id!("$original_event")),
|
||||
),
|
||||
);
|
||||
|
||||
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());
|
||||
|
||||
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;
|
||||
|
||||
timeline
|
||||
.edit_by_id(
|
||||
&hello_world_item.identifier(),
|
||||
EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain(
|
||||
"Hello, Room!",
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Let the send queue handle the event.
|
||||
yield_now().await;
|
||||
|
||||
let edit_item =
|
||||
assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => value);
|
||||
|
||||
// The event itself is already known to the server. We don't currently have
|
||||
// a separate edit send state.
|
||||
assert_matches!(edit_item.send_state(), None);
|
||||
let edit_message = edit_item.content().as_message().unwrap();
|
||||
assert_eq!(edit_message.body(), "Hello, Room!");
|
||||
assert!(edit_message.is_edited());
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_edit_by_non_existing_event_id() {
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let (client, server) = logged_in_client_with_server().await;
|
||||
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;
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
let timeline = room.timeline().await.unwrap();
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
|
||||
let error = timeline
|
||||
.edit_by_id(
|
||||
&TimelineEventItemId::EventId(owned_event_id!("$123:example.com")),
|
||||
EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain(
|
||||
"Hello, Room!",
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_matches!(error, Error::EventNotInTimeline(_));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_edit_local_echo_by_id() {
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let (client, server) = logged_in_client_with_server().await;
|
||||
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;
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
let timeline = room.timeline().await.unwrap();
|
||||
let (_, mut timeline_stream) = timeline.subscribe().await;
|
||||
|
||||
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;
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
let mounted_send = Mock::given(method("PUT"))
|
||||
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.respond_with(ResponseTemplate::new(413).set_body_json(json!({
|
||||
"errcode": "M_TOO_LARGE",
|
||||
})))
|
||||
.expect(1)
|
||||
.mount_as_scoped(&server)
|
||||
.await;
|
||||
|
||||
// Redacting a local event works.
|
||||
timeline.send(RoomMessageEventContent::text_plain("hello, just you").into()).await.unwrap();
|
||||
|
||||
assert_let!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next().await);
|
||||
|
||||
let internal_id = item.unique_id();
|
||||
|
||||
let item = item.as_event().unwrap();
|
||||
assert_matches!(item.send_state(), Some(EventSendState::NotSentYet));
|
||||
|
||||
assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await);
|
||||
assert!(day_divider.is_day_divider());
|
||||
|
||||
// We haven't set a route for sending events, so this will fail.
|
||||
|
||||
assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await);
|
||||
|
||||
let item = item.as_event().unwrap();
|
||||
assert!(item.is_local_echo());
|
||||
assert!(item.is_editable());
|
||||
|
||||
assert_matches!(
|
||||
item.send_state(),
|
||||
Some(EventSendState::SendingFailed { is_recoverable: false, .. })
|
||||
);
|
||||
|
||||
assert!(timeline_stream.next().now_or_never().is_none());
|
||||
|
||||
// Set up the success response before editing, since edit causes an immediate
|
||||
// retry (the room's send queue is not blocked, since the one event it couldn't
|
||||
// send failed in an unrecoverable way).
|
||||
drop(mounted_send);
|
||||
Mock::given(method("PUT"))
|
||||
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$1" })))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Let's edit the local echo.
|
||||
let did_edit = timeline
|
||||
.edit_by_id(
|
||||
&item.identifier(),
|
||||
EditedContent::RoomMessage(RoomMessageEventContent::text_plain("hello, world").into()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// We could edit the local echo, since it was in the failed state.
|
||||
assert!(did_edit);
|
||||
|
||||
// Observe local echo being replaced.
|
||||
assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await);
|
||||
|
||||
assert_eq!(item.unique_id(), internal_id);
|
||||
|
||||
let item = item.as_event().unwrap();
|
||||
assert!(item.is_local_echo());
|
||||
|
||||
// The send state has been reset.
|
||||
assert_matches!(item.send_state(), Some(EventSendState::NotSentYet));
|
||||
|
||||
let edit_message = item.content().as_message().unwrap();
|
||||
assert_eq!(edit_message.body(), "hello, world");
|
||||
|
||||
// Observe the event being sent, and replacing the local echo.
|
||||
assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await);
|
||||
|
||||
let item = item.as_event().unwrap();
|
||||
assert!(item.is_local_echo());
|
||||
|
||||
let edit_message = item.content().as_message().unwrap();
|
||||
assert_eq!(edit_message.body(), "hello, world");
|
||||
|
||||
// No new updates.
|
||||
assert!(timeline_stream.next().now_or_never().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_edit_by_non_existing_local_id() {
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let (client, server) = logged_in_client_with_server().await;
|
||||
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;
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
let timeline = room.timeline().await.unwrap();
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
|
||||
let error = timeline
|
||||
.edit_by_id(
|
||||
&TimelineEventItemId::TransactionId("something".into()),
|
||||
EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain(
|
||||
"Hello, Room!",
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_matches!(error, Error::EventNotInTimeline(_));
|
||||
}
|
||||
|
||||
@@ -35,18 +35,14 @@ use matrix_sdk_ui::{
|
||||
},
|
||||
RoomListService, Timeline,
|
||||
};
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::room::{encryption::RoomEncryptionEventContent, message::RoomMessageEventContent},
|
||||
room_id, user_id, MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
use ruma::{event_id, events::room::{encryption::RoomEncryptionEventContent, message::RoomMessageEventContent}, owned_event_id, room_id, user_id, MilliSecondsSinceUnixEpoch};
|
||||
use serde_json::json;
|
||||
use stream_assert::{assert_next_matches, assert_pending};
|
||||
use wiremock::{
|
||||
matchers::{header, method, path_regex},
|
||||
Mock, ResponseTemplate,
|
||||
};
|
||||
|
||||
use matrix_sdk_ui::timeline::{Error, RedactError, TimelineEventItemId};
|
||||
use crate::mock_sync;
|
||||
|
||||
mod echo;
|
||||
@@ -307,6 +303,132 @@ async fn test_redact_message() {
|
||||
assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 2 }));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_redact_by_id_message() {
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let (client, server) = logged_in_client_with_server().await;
|
||||
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;
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
let timeline = room.timeline().await.unwrap();
|
||||
let (_, mut timeline_stream) = timeline.subscribe().await;
|
||||
|
||||
let factory = EventFactory::new();
|
||||
factory.set_next_ts(MilliSecondsSinceUnixEpoch::now().get().into());
|
||||
|
||||
sync_builder.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
factory.sender(user_id!("@a:b.com")).text_msg("buy my bitcoins bro"),
|
||||
),
|
||||
);
|
||||
|
||||
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
|
||||
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
|
||||
server.reset().await;
|
||||
|
||||
assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await);
|
||||
assert_eq!(
|
||||
first.as_event().unwrap().content().as_message().unwrap().body(),
|
||||
"buy my bitcoins bro"
|
||||
);
|
||||
|
||||
assert_let!(Some(VectorDiff::PushFront { value: day_divider }) = timeline_stream.next().await);
|
||||
assert!(day_divider.is_day_divider());
|
||||
|
||||
// Redacting a remote event works.
|
||||
mock_redaction(event_id!("$42")).mount(&server).await;
|
||||
|
||||
let event = first.as_event().unwrap();
|
||||
|
||||
timeline.redact_by_id(&event.identifier(), Some("inapprops")).await.unwrap();
|
||||
|
||||
// Redacting a local event works.
|
||||
timeline
|
||||
.send(RoomMessageEventContent::text_plain("i will disappear soon").into())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_let!(Some(VectorDiff::PushBack { value: second }) = timeline_stream.next().await);
|
||||
|
||||
let second = second.as_event().unwrap();
|
||||
assert_matches!(second.send_state(), Some(EventSendState::NotSentYet));
|
||||
|
||||
// We haven't set a route for sending events, so this will fail.
|
||||
assert_let!(Some(VectorDiff::Set { index, value: second }) = timeline_stream.next().await);
|
||||
assert_eq!(index, 2);
|
||||
|
||||
let second = second.as_event().unwrap();
|
||||
assert!(second.is_local_echo());
|
||||
assert_matches!(second.send_state(), Some(EventSendState::SendingFailed { .. }));
|
||||
|
||||
// Let's redact the local echo.
|
||||
timeline.redact_by_id(&second.identifier(), None).await.unwrap();
|
||||
|
||||
// Observe local echo being removed.
|
||||
assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 2 }));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_redact_by_id_message_with_no_remote_message_present() {
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let (client, server) = logged_in_client_with_server().await;
|
||||
|
||||
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;
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
let timeline = room.timeline().await.unwrap();
|
||||
|
||||
let error = timeline
|
||||
.redact_by_id(&TimelineEventItemId::EventId(owned_event_id!("$123:example.com")), None)
|
||||
.await
|
||||
.err();
|
||||
assert_matches!(error, Some(Error::RedactError(RedactError::HttpError(_))))
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_redact_by_id_message_with_no_local_message_present() {
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
let (client, server) = logged_in_client_with_server().await;
|
||||
|
||||
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;
|
||||
|
||||
mock_encryption_state(&server, false).await;
|
||||
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
let timeline = room.timeline().await.unwrap();
|
||||
|
||||
let error = timeline
|
||||
.redact_by_id(&TimelineEventItemId::TransactionId("something".into()), None)
|
||||
.await
|
||||
.err();
|
||||
assert_matches!(error, Some(Error::RedactError(RedactError::LocalEventNotFound(_))))
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_read_marker() {
|
||||
let room_id = room_id!("!a98sd12bjh:example.org");
|
||||
|
||||
@@ -61,7 +61,7 @@ async fn test_in_reply_to_details() {
|
||||
// The event doesn't exist.
|
||||
assert_matches!(
|
||||
timeline.fetch_details_for_event(event_id!("$fakeevent")).await,
|
||||
Err(TimelineError::RemoteEventNotInTimeline)
|
||||
Err(TimelineError::EventNotInTimeline(_))
|
||||
);
|
||||
|
||||
// Add an event and a reply to that event to the timeline
|
||||
|
||||
Reference in New Issue
Block a user