matrix-rust-sdk/crates/matrix-sdk-ui/src/timeline/read_receipts.rs

306 lines
11 KiB
Rust

// Copyright 2023 Kévin Commaille
//
// 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.
use std::{collections::HashMap, sync::Arc};
use eyeball_im::ObservableVector;
use indexmap::IndexMap;
use matrix_sdk::room;
use ruma::{
events::receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType},
EventId, OwnedEventId, OwnedUserId, UserId,
};
use tracing::{error, warn};
use super::{
compare_events_positions, event_item::EventTimelineItemKind, inner::TimelineInnerState,
rfind_event_by_id, traits::RoomDataProvider, EventTimelineItem, RelativePosition, TimelineItem,
};
struct FullReceipt<'a> {
event_id: &'a EventId,
user_id: &'a UserId,
receipt_type: ReceiptType,
receipt: &'a Receipt,
}
impl TimelineInnerState {
/// Update the new item pointed to by the user's read receipt.
fn add_read_receipt(
&mut self,
receipt_item_pos: Option<usize>,
user_id: OwnedUserId,
receipt: Receipt,
) {
let Some(pos) = receipt_item_pos else { return };
let Some(mut event_item) = self.items[pos].as_event().cloned() else { return };
event_item.as_remote_mut().unwrap().add_read_receipt(user_id, receipt);
self.items.set(pos, Arc::new(event_item.into()));
}
pub(super) fn handle_explicit_read_receipts(
&mut self,
receipt_event_content: ReceiptEventContent,
own_user_id: &UserId,
) {
for (event_id, receipt_types) in receipt_event_content.0 {
for (receipt_type, receipts) in receipt_types {
// We only care about read receipts here.
if !matches!(receipt_type, ReceiptType::Read | ReceiptType::ReadPrivate) {
continue;
}
for (user_id, receipt) in receipts {
if receipt.thread != ReceiptThread::Unthreaded {
continue;
}
let receipt_item_pos =
rfind_event_by_id(&self.items, &event_id).map(|(pos, _)| pos);
let is_own_user_id = user_id == own_user_id;
let full_receipt = FullReceipt {
event_id: &event_id,
user_id: &user_id,
receipt_type: receipt_type.clone(),
receipt: &receipt,
};
let read_receipt_updated = maybe_update_read_receipt(
full_receipt,
receipt_item_pos,
is_own_user_id,
&mut self.items,
&mut self.users_read_receipts,
);
if read_receipt_updated && !is_own_user_id {
self.add_read_receipt(receipt_item_pos, user_id, receipt);
}
}
}
}
}
/// Load the read receipts from the store for the given event ID.
pub(super) async fn load_read_receipts_for_event<P: RoomDataProvider>(
&mut self,
event_id: &EventId,
room_data_provider: &P,
) -> IndexMap<OwnedUserId, Receipt> {
let read_receipts = room_data_provider.read_receipts_for_event(event_id).await;
// Filter out receipts for our own user.
let own_user_id = room_data_provider.own_user_id();
let read_receipts: IndexMap<OwnedUserId, Receipt> =
read_receipts.into_iter().filter(|(user_id, _)| user_id != own_user_id).collect();
// Keep track of the user's read receipt.
for (user_id, receipt) in read_receipts.clone() {
// Only insert the read receipt if the user is not known to avoid conflicts with
// `TimelineInner::handle_read_receipts`.
if !self.users_read_receipts.contains_key(&user_id) {
self.users_read_receipts
.entry(user_id)
.or_default()
.insert(ReceiptType::Read, (event_id.to_owned(), receipt));
}
}
read_receipts
}
/// Get the unthreaded receipt of the given type for the given user in the
/// timeline.
pub(super) async fn user_receipt(
&self,
user_id: &UserId,
receipt_type: ReceiptType,
room: &room::Common,
) -> Option<(OwnedEventId, Receipt)> {
if let Some(receipt) = self
.users_read_receipts
.get(user_id)
.and_then(|user_map| user_map.get(&receipt_type))
.cloned()
{
return Some(receipt);
}
room.user_receipt(receipt_type.clone(), ReceiptThread::Unthreaded, user_id)
.await
.unwrap_or_else(|e| {
error!("Could not get user read receipt of type {receipt_type:?}: {e}");
None
})
}
/// Get the latest read receipt for the given user.
///
/// Useful to get the latest read receipt, whether it's private or public.
pub(super) async fn latest_user_read_receipt(
&self,
user_id: &UserId,
room: &room::Common,
) -> Option<(OwnedEventId, Receipt)> {
let public_read_receipt = self.user_receipt(user_id, ReceiptType::Read, room).await;
let private_read_receipt = self.user_receipt(user_id, ReceiptType::ReadPrivate, room).await;
// If we only have one, return it.
let Some((pub_event_id, pub_receipt)) = &public_read_receipt else {
return private_read_receipt;
};
let Some((priv_event_id, priv_receipt)) = &private_read_receipt else {
return public_read_receipt;
};
// Compare by position in the timeline.
if let Some(relative_pos) =
compare_events_positions(pub_event_id, priv_event_id, &self.items)
{
if relative_pos == RelativePosition::After {
return private_read_receipt;
}
return public_read_receipt;
}
// Compare by timestamp.
if let Some((pub_ts, priv_ts)) = pub_receipt.ts.zip(priv_receipt.ts) {
if priv_ts > pub_ts {
return private_read_receipt;
}
return public_read_receipt;
}
// As a fallback, let's assume that a private read receipt should be more recent
// than a public read receipt, otherwise there's no point in the private read
// receipt.
private_read_receipt
}
}
/// Add an implicit read receipt to the given event item, if it is more recent
/// than the current read receipt for the sender of the event.
///
/// According to the spec, read receipts should not point to events sent by our
/// own user, but these events are used to reset the notification count, so we
/// need to handle them locally too. For that we create an "implicit" read
/// receipt, compared to the "explicit" ones sent by the client.
pub(super) fn maybe_add_implicit_read_receipt(
item_pos: usize,
event_item: &mut EventTimelineItem,
is_own_event: bool,
timeline_items: &mut ObservableVector<Arc<TimelineItem>>,
users_read_receipts: &mut HashMap<OwnedUserId, HashMap<ReceiptType, (OwnedEventId, Receipt)>>,
) {
let EventTimelineItemKind::Remote(remote_event_item) = &mut event_item.kind else {
return;
};
let receipt = Receipt::new(event_item.timestamp);
let new_receipt = FullReceipt {
event_id: &remote_event_item.event_id,
user_id: &event_item.sender,
receipt_type: ReceiptType::Read,
receipt: &receipt,
};
let read_receipt_updated = maybe_update_read_receipt(
new_receipt,
Some(item_pos),
is_own_event,
timeline_items,
users_read_receipts,
);
if read_receipt_updated && !is_own_event {
remote_event_item.add_read_receipt(event_item.sender.clone(), receipt);
}
}
/// Update the timeline items with the given read receipt if it is more recent
/// than the current one.
///
/// In the process, this method removes the corresponding receipt from its old
/// item, if applicable, and updates the `users_read_receipts` map to use the
/// new receipt.
///
/// Returns true if the read receipt was saved.
///
/// Currently this method only works reliably if the timeline was started from
/// the end of the timeline.
fn maybe_update_read_receipt(
receipt: FullReceipt<'_>,
new_item_pos: Option<usize>,
is_own_user_id: bool,
timeline_items: &mut ObservableVector<Arc<TimelineItem>>,
users_read_receipts: &mut HashMap<OwnedUserId, HashMap<ReceiptType, (OwnedEventId, Receipt)>>,
) -> bool {
let old_event_id = users_read_receipts
.get(receipt.user_id)
.and_then(|receipts| receipts.get(&receipt.receipt_type))
.map(|(event_id, _)| event_id);
if old_event_id.map_or(false, |id| id == receipt.event_id) {
// Nothing to do.
return false;
}
let old_item_and_pos = old_event_id.and_then(|e| rfind_event_by_id(timeline_items, e));
if let Some((old_receipt_pos, old_event_item)) = old_item_and_pos {
let Some(new_receipt_pos) = new_item_pos else {
// The old receipt is likely more recent since we can't find the
// event of the new receipt in the timeline. Even if it isn't, we
// wouldn't know where to put it.
return false;
};
if old_receipt_pos > new_receipt_pos {
// The old receipt is more recent than the new one.
return false;
}
if !is_own_user_id {
// Remove the read receipt for this user from the old event.
let mut old_event_item = old_event_item.clone();
if let Some(old_remote_event_item) = old_event_item.as_remote_mut() {
if !old_remote_event_item.remove_read_receipt(receipt.user_id) {
error!(
"inconsistent state: old event item for user's read \
receipt doesn't have a receipt for the user"
);
}
timeline_items.set(old_receipt_pos, Arc::new(old_event_item.into()));
} else {
warn!("received a read receipt for a local item, this should not be possible");
}
}
}
// The new receipt is deemed more recent from now on because:
// - If old_receipt_item is Some, we already checked all the cases where it
// wouldn't be more recent.
// - If both old_receipt_item and new_receipt_item are None, they are both
// explicit read receipts so the server should only send us a more recent
// receipt.
// - If old_receipt_item is None and new_receipt_item is Some, the new receipt
// is likely more recent because it has a place in the timeline.
users_read_receipts
.entry(receipt.user_id.to_owned())
.or_default()
.insert(receipt.receipt_type, (receipt.event_id.to_owned(), receipt.receipt.clone()));
true
}