Skip to content

Commit 7a6e29c

Browse files
committed
feat(ui): don't mark each thread reply as an actual reply to the previous message, in threaded timelines
This correctly handles the reply fallback behavior: - the behavior isn't changed for a live timeline, - when a timeline is thread-focused, we will extract the `replied_to` field if and only if the thread relation is *not* marked as behaving in a fallback manner. This makes it possible to distinguish actual in-thread replies.
1 parent 216e878 commit 7a6e29c

5 files changed

Lines changed: 62 additions & 20 deletions

File tree

crates/matrix-sdk-ui/src/timeline/controller/metadata.rs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use super::{
3838
};
3939
use crate::{
4040
timeline::{
41+
controller::TimelineFocusKind,
4142
event_item::{
4243
extract_bundled_edit_event_json, extract_poll_edit_content,
4344
extract_room_msg_edit_content,
@@ -316,6 +317,7 @@ impl TimelineMetadata {
316317
raw_event: &Raw<AnySyncTimelineEvent>,
317318
bundled_edit_encryption_info: Option<Arc<EncryptionInfo>>,
318319
timeline_items: &Vector<Arc<TimelineItem>>,
320+
timeline_focus: &TimelineFocusKind,
319321
) -> (Option<InReplyToDetails>, Option<OwnedEventId>) {
320322
if let AnySyncTimelineEvent::MessageLike(ev) = event {
321323
if let Some(content) = ev.original_content() {
@@ -325,7 +327,12 @@ impl TimelineMetadata {
325327
relations: ev.relations(),
326328
bundled_edit_encryption_info,
327329
});
328-
return self.process_content_relations(&content, remote_ctx, timeline_items);
330+
return self.process_content_relations(
331+
&content,
332+
remote_ctx,
333+
timeline_items,
334+
timeline_focus,
335+
);
329336
}
330337
}
331338
(None, None)
@@ -341,12 +348,14 @@ impl TimelineMetadata {
341348
content: &AnyMessageLikeEventContent,
342349
remote_ctx: Option<RemoteEventContext<'_>>,
343350
timeline_items: &Vector<Arc<TimelineItem>>,
351+
timeline_focus: &TimelineFocusKind,
344352
) -> (Option<InReplyToDetails>, Option<OwnedEventId>) {
345353
match content {
346354
AnyMessageLikeEventContent::Sticker(content) => {
347355
let (in_reply_to, thread_root) = Self::extract_reply_and_thread_root(
348356
content.relates_to.clone().and_then(|rel| rel.try_into().ok()),
349357
timeline_items,
358+
timeline_focus,
350359
);
351360

352361
if let Some(event_id) = remote_ctx.map(|ctx| ctx.event_id) {
@@ -359,8 +368,11 @@ impl TimelineMetadata {
359368
AnyMessageLikeEventContent::UnstablePollStart(UnstablePollStartEventContent::New(
360369
c,
361370
)) => {
362-
let (in_reply_to, thread_root) =
363-
Self::extract_reply_and_thread_root(c.relates_to.clone(), timeline_items);
371+
let (in_reply_to, thread_root) = Self::extract_reply_and_thread_root(
372+
c.relates_to.clone(),
373+
timeline_items,
374+
timeline_focus,
375+
);
364376

365377
// Record the bundled edit in the aggregations set, if any.
366378
if let Some(ctx) = remote_ctx {
@@ -398,6 +410,7 @@ impl TimelineMetadata {
398410
let (in_reply_to, thread_root) = Self::extract_reply_and_thread_root(
399411
msg.relates_to.clone().and_then(|rel| rel.try_into().ok()),
400412
timeline_items,
413+
timeline_focus,
401414
);
402415

403416
// Record the bundled edit in the aggregations set, if any.
@@ -441,6 +454,7 @@ impl TimelineMetadata {
441454
fn extract_reply_and_thread_root(
442455
relates_to: Option<RelationWithoutReplacement>,
443456
timeline_items: &Vector<Arc<TimelineItem>>,
457+
timeline_focus: &TimelineFocusKind,
444458
) -> (Option<InReplyToDetails>, Option<OwnedEventId>) {
445459
let mut thread_root = None;
446460

@@ -450,9 +464,25 @@ impl TimelineMetadata {
450464
}
451465
RelationWithoutReplacement::Thread(thread) => {
452466
thread_root = Some(thread.event_id);
453-
thread
454-
.in_reply_to
455-
.map(|in_reply_to| InReplyToDetails::new(in_reply_to.event_id, timeline_items))
467+
468+
if matches!(timeline_focus, TimelineFocusKind::Thread { .. })
469+
&& thread.is_falling_back
470+
{
471+
// In general, a threaded event is marked as a response to the previous message
472+
// in the thread, to maintain backwards compatibility with clients not
473+
// supporting threads.
474+
//
475+
// But we can have actual replies to other in-thread events. The
476+
// `is_falling_back` bool helps distinguishing both use cases.
477+
//
478+
// If this timeline is thread-focused, we only mark non-falling-back replies as
479+
// actual in-thread replies.
480+
None
481+
} else {
482+
thread.in_reply_to.map(|in_reply_to| {
483+
InReplyToDetails::new(in_reply_to.event_id, timeline_items)
484+
})
485+
}
456486
}
457487
_ => None,
458488
});

crates/matrix-sdk-ui/src/timeline/controller/state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ impl TimelineState {
165165
let mut date_divider_adjuster = DateDividerAdjuster::new(date_divider_mode);
166166

167167
let (in_reply_to, thread_root) =
168-
txn.meta.process_content_relations(&content, None, &txn.items);
168+
txn.meta.process_content_relations(&content, None, &txn.items, &txn.timeline_focus);
169169

170170
// TODO merge with other should_add, one way or another?
171171
let should_add_new_items = match &txn.timeline_focus {

crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ impl<'a> TimelineStateTransaction<'a> {
626626
&raw,
627627
bundled_edit_encryption_info,
628628
&self.items,
629+
&self.timeline_focus,
629630
);
630631

631632
let should_add = self.should_add_event_item(

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ async fn test_thread_backpagination() {
102102
factory
103103
.text_msg("Threaded event 4")
104104
.event_id(event_id!("$4"))
105-
.in_thread(&thread_root_event_id, event_id!("$3"))
105+
.in_thread_reply(&thread_root_event_id, event_id!("$2"))
106106
.into_raw_sync()
107107
.cast(),
108108
factory
@@ -159,14 +159,17 @@ async fn test_thread_backpagination() {
159159
assert_eq!(items.len(), 2 + 1); // A date divider + the 2 events
160160
assert!(items[0].is_date_divider());
161161

162-
assert_eq!(
163-
items[1].as_event().unwrap().content().as_message().unwrap().body(),
164-
"Threaded event 3"
165-
);
166-
assert_eq!(
167-
items[2].as_event().unwrap().content().as_message().unwrap().body(),
168-
"Threaded event 4"
169-
);
162+
let event_item = items[1].as_event().unwrap();
163+
assert_eq!(event_item.content().as_message().unwrap().body(), "Threaded event 3");
164+
// In a threaded timeline, threads aren't using the reply fallback, unless
165+
// they're an actual reply to another thread event.
166+
assert_matches!(event_item.content().in_reply_to(), None);
167+
168+
let event_item = items[2].as_event().unwrap();
169+
assert_eq!(event_item.content().as_message().unwrap().body(), "Threaded event 4");
170+
// But this one is an actual reply to another thread event, so it has the
171+
// replied-to event correctly set.
172+
assert_eq!(event_item.content().in_reply_to().unwrap().event_id, event_id!("$2"));
170173

171174
let hit_start = timeline.paginate_backwards(100).await.unwrap();
172175
assert!(hit_start);
@@ -181,12 +184,12 @@ async fn test_thread_backpagination() {
181184
assert_let!(VectorDiff::PushFront { value } = &timeline_updates[0]);
182185
let event_item = value.as_event().unwrap();
183186
assert_eq!(event_item.event_id().unwrap(), event_id!("$2"));
184-
assert_eq!(event_item.content().in_reply_to().unwrap().event_id, event_id!("$1"));
187+
assert_matches!(event_item.content().in_reply_to(), None);
185188

186189
assert_let!(VectorDiff::PushFront { value } = &timeline_updates[1]);
187190
let event_item = value.as_event().unwrap();
188191
assert_eq!(event_item.event_id().unwrap(), event_id!("$1"));
189-
assert_eq!(event_item.content().in_reply_to().unwrap().event_id, event_id!("$root"));
192+
assert_matches!(event_item.content().in_reply_to(), None);
190193

191194
assert_let!(VectorDiff::PushFront { value } = &timeline_updates[2]);
192195
assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$root"));

testing/matrix-sdk-test/src/event_factory.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,14 +345,22 @@ impl EventBuilder<RoomMessageEventContent> {
345345
self
346346
}
347347

348-
/// Adds a thread relation to the root event, setting the latest thread
349-
/// event id too.
348+
/// Adds a thread relation to the root event, setting the reply fallback to
349+
/// the latest in-thread event.
350350
pub fn in_thread(mut self, root: &EventId, latest_thread_event: &EventId) -> Self {
351351
self.content.relates_to =
352352
Some(Relation::Thread(Thread::plain(root.to_owned(), latest_thread_event.to_owned())));
353353
self
354354
}
355355

356+
/// Adds a thread relation to the root event, that's a non-fallback reply to
357+
/// another thread event.
358+
pub fn in_thread_reply(mut self, root: &EventId, replied_to: &EventId) -> Self {
359+
self.content.relates_to =
360+
Some(Relation::Thread(Thread::reply(root.to_owned(), replied_to.to_owned())));
361+
self
362+
}
363+
356364
/// Adds a replacement relation to the current event, with the new content
357365
/// passed.
358366
pub fn edit(

0 commit comments

Comments
 (0)