fix(relations): Fix duplicate messages in thread pagination

The /relations endpoint was incorrectly modifying the pagination starting
point with from.saturating_inc(dir), causing duplicate events to appear
when scrolling up in message threads. This was particularly noticeable
when viewing replies in Element.

The fix removes the incorrect increment/decrement logic and adds proper
filtering to exclude the starting event itself from results. The endpoint
now uses ShortEventId directly as the pagination token, avoiding issues
with PduCount in federated environments where events can arrive out of
order.

Fixes: #814
This commit is contained in:
Tom Foster 2025-08-10 12:40:29 +01:00
commit 5dbb5b015c
2 changed files with 19 additions and 9 deletions

View file

@ -109,13 +109,13 @@ async fn paginate_relations_with_filter(
recurse: bool,
dir: Direction,
) -> Result<get_relating_events::v1::Response> {
let start: PduCount = from
.map(str::parse)
.transpose()?
.unwrap_or_else(|| match dir {
let start: PduCount = from.and_then(|s| s.parse::<u64>().ok()).map_or_else(
|| match dir {
| Direction::Forward => PduCount::min(),
| Direction::Backward => PduCount::max(),
});
},
PduCount::Normal,
);
let to: Option<PduCount> = to.map(str::parse).flat_ok();
@ -156,9 +156,7 @@ async fn paginate_relations_with_filter(
| Direction::Forward => events.last(),
| Direction::Backward => events.first(),
}
.map(at!(0))
.as_ref()
.map(ToString::to_string);
.map(|(count, _)| count.into_unsigned().to_string());
Ok(get_relating_events::v1::Response {
next_batch,

View file

@ -61,9 +61,10 @@ impl Data {
from: PduCount,
dir: Direction,
) -> impl Stream<Item = (PduCount, impl Event)> + Send + '_ {
let from_unsigned = from.into_unsigned();
let mut current = ArrayVec::<u8, 16>::new();
current.extend(target.to_be_bytes());
current.extend(from.saturating_inc(dir).into_unsigned().to_be_bytes());
current.extend(from_unsigned.to_be_bytes());
let current = current.as_slice();
match dir {
| Direction::Forward => self.tofrom_relation.raw_keys_from(current).boxed(),
@ -73,6 +74,17 @@ impl Data {
.ready_take_while(move |key| key.starts_with(&target.to_be_bytes()))
.map(|to_from| u64_from_u8(&to_from[8..16]))
.map(PduCount::from_unsigned)
.ready_filter(move |count| {
if from == PduCount::min() || from == PduCount::max() {
true
} else {
let count_unsigned = count.into_unsigned();
match dir {
| Direction::Forward => count_unsigned > from_unsigned,
| Direction::Backward => count_unsigned < from_unsigned,
}
}
})
.wide_filter_map(move |shorteventid| async move {
let pdu_id: RawPduId = PduId { shortroomid, shorteventid }.into();