events: Add support for read receipts for threads
According to MSC3771
This commit is contained in:
parent
e6e7e52034
commit
d3bd86a23e
@ -22,6 +22,7 @@ Improvements:
|
||||
* Add parameter to `RoomMessageEventContent::make_reply_to` to be thread-aware
|
||||
* Add `RoomMessageEventContent::make_for_reply`
|
||||
* Stabilize support for event replacements (edits)
|
||||
* Add support for read receipts for threads (MSC3771 / Matrix 1.4)
|
||||
|
||||
# 0.10.3
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
//!
|
||||
//! [`m.receipt`]: https://spec.matrix.org/v1.2/client-server-api/#mreceipt
|
||||
|
||||
mod receipt_thread_serde;
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
ops::{Deref, DerefMut},
|
||||
@ -10,7 +12,10 @@ use std::{
|
||||
use ruma_macros::{EventContent, OrdAsRefStr, PartialEqAsRefStr, PartialOrdAsRefStr, StringEnum};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, PrivOwnedStr, UserId};
|
||||
use crate::{
|
||||
EventId, IdParseError, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, PrivOwnedStr,
|
||||
UserId,
|
||||
};
|
||||
|
||||
/// The content of an `m.receipt` event.
|
||||
///
|
||||
@ -102,6 +107,10 @@ pub struct Receipt {
|
||||
/// The time when the receipt was sent.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||
|
||||
/// The thread this receipt applies to.
|
||||
#[serde(rename = "thread_id", default, skip_serializing_if = "crate::serde::is_default")]
|
||||
pub thread: ReceiptThread,
|
||||
}
|
||||
|
||||
impl Receipt {
|
||||
@ -109,6 +118,140 @@ impl Receipt {
|
||||
///
|
||||
/// To create an empty receipt instead, use [`Receipt::default`].
|
||||
pub fn new(ts: MilliSecondsSinceUnixEpoch) -> Self {
|
||||
Self { ts: Some(ts) }
|
||||
Self { ts: Some(ts), thread: ReceiptThread::Unthreaded }
|
||||
}
|
||||
}
|
||||
|
||||
/// The [thread a receipt applies to].
|
||||
///
|
||||
/// This type can hold an arbitrary string. To build this with a custom value, convert it from an
|
||||
/// `Option<String>` with `::from()` / `.into()`. [`ReceiptThread::Unthreaded`] can be constructed
|
||||
/// from `None`.
|
||||
///
|
||||
/// To check for values that are not available as a documented variant here, use its string
|
||||
/// representation, obtained through [`.as_str()`](Self::as_str()).
|
||||
///
|
||||
/// [thread a receipt applies to]: https://spec.matrix.org/v1.4/client-server-api/#threaded-read-receipts
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ReceiptThread {
|
||||
/// The receipt applies to the timeline, regardless of threads.
|
||||
///
|
||||
/// Used by clients that are not aware of threads.
|
||||
///
|
||||
/// This is the default.
|
||||
#[default]
|
||||
Unthreaded,
|
||||
|
||||
/// The receipt applies to the main timeline.
|
||||
///
|
||||
/// Used for events that don't belong to a thread.
|
||||
Main,
|
||||
|
||||
/// The receipt applies to a thread.
|
||||
///
|
||||
/// Used for events that belong to a thread with the given thread root.
|
||||
Thread(OwnedEventId),
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
impl ReceiptThread {
|
||||
/// Get the string representation of this `ReceiptThread`.
|
||||
///
|
||||
/// [`ReceiptThread::Unthreaded`] returns `None`.
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Unthreaded => None,
|
||||
Self::Main => Some("main"),
|
||||
Self::Thread(event_id) => Some(event_id.as_str()),
|
||||
Self::_Custom(s) => Some(&s.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TryFrom<Option<T>> for ReceiptThread
|
||||
where
|
||||
T: AsRef<str> + Into<Box<str>>,
|
||||
{
|
||||
type Error = IdParseError;
|
||||
|
||||
fn try_from(s: Option<T>) -> Result<Self, Self::Error> {
|
||||
let res = match s {
|
||||
None => Self::Unthreaded,
|
||||
Some(s) => match s.as_ref() {
|
||||
"main" => Self::Main,
|
||||
s_ref if s_ref.starts_with('$') => Self::Thread(EventId::parse(s_ref)?),
|
||||
_ => Self::_Custom(PrivOwnedStr(s.into())),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
||||
|
||||
use super::{Receipt, ReceiptThread};
|
||||
use crate::{event_id, MilliSecondsSinceUnixEpoch};
|
||||
|
||||
#[test]
|
||||
fn serialize_receipt() {
|
||||
let mut receipt = Receipt::default();
|
||||
assert_eq!(to_json_value(receipt.clone()).unwrap(), json!({}));
|
||||
|
||||
receipt.thread = ReceiptThread::Main;
|
||||
assert_eq!(to_json_value(receipt.clone()).unwrap(), json!({ "thread_id": "main" }));
|
||||
|
||||
receipt.thread = ReceiptThread::Thread(event_id!("$abcdef76543").to_owned());
|
||||
assert_eq!(to_json_value(receipt).unwrap(), json!({ "thread_id": "$abcdef76543" }));
|
||||
|
||||
let mut receipt =
|
||||
Receipt::new(MilliSecondsSinceUnixEpoch(1_664_702_144_365_u64.try_into().unwrap()));
|
||||
assert_eq!(to_json_value(receipt.clone()).unwrap(), json!({ "ts": 1_664_702_144_365_u64 }));
|
||||
|
||||
receipt.thread = ReceiptThread::try_from(Some("io.ruma.unknown")).unwrap();
|
||||
assert_eq!(
|
||||
to_json_value(receipt).unwrap(),
|
||||
json!({ "ts": 1_664_702_144_365_u64, "thread_id": "io.ruma.unknown" })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_receipt() {
|
||||
let receipt = from_json_value::<Receipt>(json!({})).unwrap();
|
||||
assert_eq!(receipt.ts, None);
|
||||
assert_eq!(receipt.thread, ReceiptThread::Unthreaded);
|
||||
|
||||
let receipt = from_json_value::<Receipt>(json!({ "thread_id": "main" })).unwrap();
|
||||
assert_eq!(receipt.ts, None);
|
||||
assert_eq!(receipt.thread, ReceiptThread::Main);
|
||||
|
||||
let receipt = from_json_value::<Receipt>(json!({ "thread_id": "$abcdef76543" })).unwrap();
|
||||
assert_eq!(receipt.ts, None);
|
||||
let event_id = assert_matches!(receipt.thread, ReceiptThread::Thread(event_id) => event_id);
|
||||
assert_eq!(event_id, "$abcdef76543");
|
||||
|
||||
let receipt = from_json_value::<Receipt>(json!({ "ts": 1_664_702_144_365_u64 })).unwrap();
|
||||
assert_eq!(
|
||||
receipt.ts.unwrap(),
|
||||
MilliSecondsSinceUnixEpoch(1_664_702_144_365_u64.try_into().unwrap())
|
||||
);
|
||||
assert_eq!(receipt.thread, ReceiptThread::Unthreaded);
|
||||
|
||||
let receipt = from_json_value::<Receipt>(
|
||||
json!({ "ts": 1_664_702_144_365_u64, "thread_id": "io.ruma.unknown" }),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
receipt.ts.unwrap(),
|
||||
MilliSecondsSinceUnixEpoch(1_664_702_144_365_u64.try_into().unwrap())
|
||||
);
|
||||
assert_matches!(receipt.thread, ReceiptThread::_Custom(_));
|
||||
assert_eq!(receipt.thread.as_str().unwrap(), "io.ruma.unknown");
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use super::ReceiptThread;
|
||||
|
||||
impl Serialize for ReceiptThread {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.as_str().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ReceiptThread {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = crate::serde::deserialize_cow_str(deserializer)?;
|
||||
Self::try_from(Some(s)).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user