Skip to content

Commit cba711d

Browse files
committed
feat(timeline): add support for sending sticker/polls in thread automatically too
1 parent e87e933 commit cba711d

File tree

2 files changed

+218
-25
lines changed

2 files changed

+218
-25
lines changed

crates/matrix-sdk-ui/src/timeline/mod.rs

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@ use ruma::{
4747
AnyMessageLikeEventContent, AnySyncTimelineEvent, Mentions,
4848
poll::unstable_start::{NewUnstablePollStartEventContent, UnstablePollStartEventContent},
4949
receipt::{Receipt, ReceiptThread},
50+
relation::Thread,
5051
room::{
51-
message::{FormattedBody, ReplyWithinThread, RoomMessageEventContentWithoutRelation},
52+
message::{
53+
FormattedBody, Relation, RelationWithoutReplacement, ReplyWithinThread,
54+
RoomMessageEventContentWithoutRelation,
55+
},
5256
pinned_events::RoomPinnedEventsEventContent,
5357
},
5458
},
@@ -294,31 +298,50 @@ impl Timeline {
294298
///
295299
/// * `content` - The content of the message event.
296300
#[instrument(skip(self, content), fields(room_id = ?self.room().room_id()))]
297-
pub async fn send(&self, content: AnyMessageLikeEventContent) -> Result<SendHandle, Error> {
301+
pub async fn send(&self, mut content: AnyMessageLikeEventContent) -> Result<SendHandle, Error> {
298302
// If this is a room event we're sending in a threaded timeline, we add the
299303
// thread relation ourselves.
300-
if let AnyMessageLikeEventContent::RoomMessage(ref room_msg_content) = content
301-
&& room_msg_content.relates_to.is_none()
302-
&& self.controller.is_threaded()
304+
if content.relation().is_none()
305+
&& let Some(reply) = self.infer_reply(None).await
303306
{
304-
let reply = self
305-
.infer_reply(None)
306-
.await
307-
.expect("a reply will always be set for threaded timelines");
308-
let content = self
309-
.room()
310-
.make_reply_event(
311-
// Note: this `.into()` gets rid of the relation, but we've checked previously
312-
// that the `relates_to` field wasn't set.
313-
room_msg_content.clone().into(),
314-
reply,
315-
)
316-
.await?;
317-
Ok(self.room().send_queue().send(content.into()).await?)
318-
} else {
319-
// Otherwise, we send the message as is.
320-
Ok(self.room().send_queue().send(content).await?)
307+
match &mut content {
308+
AnyMessageLikeEventContent::RoomMessage(room_msg_content) => {
309+
content = self
310+
.room()
311+
.make_reply_event(
312+
// Note: this `.into()` gets rid of the relation, but we've checked
313+
// previously that the `relates_to` field wasn't
314+
// set.
315+
room_msg_content.clone().into(),
316+
reply,
317+
)
318+
.await?
319+
.into();
320+
}
321+
322+
AnyMessageLikeEventContent::UnstablePollStart(
323+
UnstablePollStartEventContent::New(poll),
324+
) => {
325+
if let Some(thread_root) = self.controller.thread_root() {
326+
poll.relates_to = Some(RelationWithoutReplacement::Thread(Thread::plain(
327+
thread_root,
328+
reply.event_id,
329+
)));
330+
}
331+
}
332+
333+
AnyMessageLikeEventContent::Sticker(sticker) => {
334+
if let Some(thread_root) = self.controller.thread_root() {
335+
sticker.relates_to =
336+
Some(Relation::Thread(Thread::plain(thread_root, reply.event_id)));
337+
}
338+
}
339+
340+
_ => {}
341+
}
321342
}
343+
344+
Ok(self.room().send_queue().send(content).await?)
322345
}
323346

324347
/// Send a reply to the given event.
@@ -341,7 +364,7 @@ impl Timeline {
341364
///
342365
/// * `content` - The content of the reply.
343366
///
344-
/// * `event_id` - The ID of the event to reply to.
367+
/// * `in_reply_to` - The ID of the event to reply to.
345368
#[instrument(skip(self, content))]
346369
pub async fn send_reply(
347370
&self,

crates/matrix-sdk-ui/tests/integration/timeline/thread.rs

Lines changed: 172 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,18 @@ use ruma::{
3232
event_id,
3333
events::{
3434
AnySyncTimelineEvent,
35+
poll::unstable_start::{
36+
NewUnstablePollStartEventContent, UnstablePollAnswer, UnstablePollStartContentBlock,
37+
UnstablePollStartEventContent,
38+
},
3539
receipt::{ReceiptThread, ReceiptType},
36-
room::message::{Relation, ReplacementMetadata, RoomMessageEventContent},
40+
room::{
41+
ImageInfo,
42+
message::{Relation, ReplacementMetadata, RoomMessageEventContent},
43+
},
44+
sticker::{StickerEventContent, StickerMediaSource},
3745
},
38-
owned_event_id, room_id, user_id,
46+
owned_event_id, owned_mxc_uri, room_id, user_id,
3947
};
4048
use stream_assert::assert_pending;
4149
use tokio::task::yield_now;
@@ -893,6 +901,168 @@ async fn test_thread_timeline_can_send_edit() {
893901
assert_pending!(stream);
894902
}
895903

904+
#[async_test]
905+
async fn test_send_sticker_thread() {
906+
// If I send a sticker to a threaded timeline, it just works (aka the system to
907+
// set the threaded relationship does kick in).
908+
909+
let server = MatrixMockServer::new().await;
910+
let client = server.client_builder().build().await;
911+
912+
let room_id = room_id!("!a:b.c");
913+
let thread_root_event_id = owned_event_id!("$root");
914+
let threaded_event_id = event_id!("$threaded_event");
915+
916+
let room = server.sync_joined_room(&client, room_id).await;
917+
918+
let timeline = room
919+
.timeline_builder()
920+
.with_focus(TimelineFocus::Thread { root_event_id: thread_root_event_id.clone() })
921+
.build()
922+
.await
923+
.unwrap();
924+
925+
let (initial_items, mut stream) = timeline.subscribe().await;
926+
927+
// At first, the timeline is empty.
928+
assert!(initial_items.is_empty());
929+
assert_pending!(stream);
930+
931+
// Start the timeline with an in-thread event.
932+
let f = EventFactory::new();
933+
server
934+
.sync_room(
935+
&client,
936+
JoinedRoomBuilder::new(room_id).add_timeline_event(
937+
f.text_msg("hello world")
938+
.sender(*ALICE)
939+
.event_id(threaded_event_id)
940+
.in_thread(&thread_root_event_id, threaded_event_id)
941+
.server_ts(MilliSecondsSinceUnixEpoch::now()),
942+
),
943+
)
944+
.await;
945+
946+
// Sanity check: I receive the event and the date divider.
947+
assert_let_timeout!(Some(timeline_updates) = stream.next());
948+
assert_eq!(timeline_updates.len(), 2);
949+
950+
server.mock_room_state_encryption().plain().mount().await;
951+
952+
let sent_event_id = event_id!("$sent_msg");
953+
server.mock_room_send().ok(sent_event_id).mount().await;
954+
955+
let media_src = owned_mxc_uri!("mxc://example.com/1");
956+
timeline
957+
.send(
958+
StickerEventContent::new("sticker!".to_owned(), ImageInfo::new(), media_src.clone())
959+
.into(),
960+
)
961+
.await
962+
.unwrap();
963+
964+
// I get the local echo for the in-thread event.
965+
assert_let_timeout!(Some(timeline_updates) = stream.next());
966+
assert_eq!(timeline_updates.len(), 1);
967+
968+
assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]);
969+
let event_item = value.as_event().unwrap();
970+
971+
// The content matches what we expect.
972+
let sticker_item = event_item.content().as_sticker().unwrap();
973+
let content = sticker_item.content();
974+
assert_eq!(content.body, "sticker!");
975+
assert_let!(StickerMediaSource::Plain(plain) = content.source.clone());
976+
assert_eq!(plain, media_src);
977+
978+
// Then we're done.
979+
assert_pending!(stream);
980+
}
981+
982+
#[async_test]
983+
async fn test_send_poll_thread() {
984+
// If I send a poll to a threaded timeline, it just works (aka the system to
985+
// set the threaded relationship does kick in).
986+
987+
let server = MatrixMockServer::new().await;
988+
let client = server.client_builder().build().await;
989+
990+
let room_id = room_id!("!a:b.c");
991+
let thread_root_event_id = owned_event_id!("$root");
992+
let threaded_event_id = event_id!("$threaded_event");
993+
994+
let room = server.sync_joined_room(&client, room_id).await;
995+
996+
let timeline = room
997+
.timeline_builder()
998+
.with_focus(TimelineFocus::Thread { root_event_id: thread_root_event_id.clone() })
999+
.build()
1000+
.await
1001+
.unwrap();
1002+
1003+
let (initial_items, mut stream) = timeline.subscribe().await;
1004+
1005+
// At first, the timeline is empty.
1006+
assert!(initial_items.is_empty());
1007+
assert_pending!(stream);
1008+
1009+
// Start the timeline with an in-thread event.
1010+
let f = EventFactory::new();
1011+
server
1012+
.sync_room(
1013+
&client,
1014+
JoinedRoomBuilder::new(room_id).add_timeline_event(
1015+
f.text_msg("hello world")
1016+
.sender(*ALICE)
1017+
.event_id(threaded_event_id)
1018+
.in_thread(&thread_root_event_id, threaded_event_id)
1019+
.server_ts(MilliSecondsSinceUnixEpoch::now()),
1020+
),
1021+
)
1022+
.await;
1023+
1024+
// Sanity check: I receive the event and the date divider.
1025+
assert_let_timeout!(Some(timeline_updates) = stream.next());
1026+
assert_eq!(timeline_updates.len(), 2);
1027+
1028+
server.mock_room_state_encryption().plain().mount().await;
1029+
1030+
let sent_event_id = event_id!("$sent_msg");
1031+
server.mock_room_send().ok(sent_event_id).mount().await;
1032+
1033+
timeline
1034+
.send(
1035+
UnstablePollStartEventContent::New(NewUnstablePollStartEventContent::plain_text(
1036+
"let's vote",
1037+
UnstablePollStartContentBlock::new(
1038+
"what day is it today?",
1039+
vec![
1040+
UnstablePollAnswer::new("0", "monday"),
1041+
UnstablePollAnswer::new("1", "friday"),
1042+
]
1043+
.try_into()
1044+
.unwrap(),
1045+
),
1046+
))
1047+
.into(),
1048+
)
1049+
.await
1050+
.unwrap();
1051+
1052+
// I get the local echo for the in-thread event.
1053+
assert_let_timeout!(Some(timeline_updates) = stream.next());
1054+
assert_eq!(timeline_updates.len(), 1);
1055+
1056+
assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]);
1057+
let event_item = value.as_event().unwrap();
1058+
1059+
// The content is a poll.
1060+
assert!(event_item.content().is_poll());
1061+
1062+
// Then we're done.
1063+
assert_pending!(stream);
1064+
}
1065+
8961066
#[async_test]
8971067
async fn test_sending_read_receipt_with_no_events_doesnt_unset_read_flag() {
8981068
// If a thread timeline has no events, then marking it as read doesn't unset the

0 commit comments

Comments
 (0)