ffi: Turn EventTimelineItem into a record type

This improves parsing times in mobile Clients. On Android, this means a 5-10x faster parsing of timeline events.

To do that I had to:

- Make functions like `edit/redact/forward` take an identifier (EventId/TransactionId) instead of the actual event. This id will be used to look for the actual SDK timeline event in the timeline. This change will make these functions a bit less performant.
- Make `InReplyToDetails` an object instead since a record can't recursively contain itself.
- Turn `EventTimelineItem` into a record type. Do the same with `Message`, which is now `MessageContent`.
This commit is contained in:
Jorge Martín
2024-09-20 10:00:51 +02:00
committed by Jorge Martin Espinosa
parent e61fb45504
commit 67df36f733
6 changed files with 199 additions and 173 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
// TODO: target-os conditional would be good.
#![allow(unused_qualifications, clippy::new_without_default)]
#![allow(unused_qualifications, clippy::new_without_default, unused_macros)]
macro_rules! unwrap_or_clone_arc_into_variant {
(
+2 -2
View File
@@ -702,8 +702,8 @@ impl RoomListItem {
self.inner.is_encrypted().await.unwrap_or(false)
}
async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new)
async fn latest_event(&self) -> Option<EventTimelineItem> {
self.inner.latest_event().await.map(|e| e.into())
}
}
+12
View File
@@ -54,6 +54,7 @@ use tracing::info;
use crate::{
error::{ClientError, MediaInfoError},
helpers::unwrap_or_clone_arc,
timeline::MessageContent,
utils::u64_to_uint,
};
@@ -227,6 +228,7 @@ pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWi
}
}
#[derive(Clone)]
pub struct Mentions {
pub user_ids: Vec<String>,
pub room: bool,
@@ -861,3 +863,13 @@ impl From<RumaPollKind> for PollKind {
}
}
}
/// Creates a [`RoomMessageEventContentWithoutRelation`] given a
/// [`MessageContent`] value.
#[uniffi::export]
pub fn content_without_relation_from_message(
message: MessageContent,
) -> Result<RoomMessageEventContentWithoutRelation, ClientError> {
let msg_type = message.msg_type.try_into()?;
Ok(RoomMessageEventContentWithoutRelation::new(msg_type))
}
+69 -64
View File
@@ -16,45 +16,40 @@ use std::{collections::HashMap, sync::Arc};
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
use ruma::events::room::{message::RoomMessageEventContentWithoutRelation, MediaSource};
use tracing::warn;
use ruma::events::room::MediaSource;
use super::ProfileDetails;
use crate::ruma::{ImageInfo, MessageType, PollKind};
use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind};
#[derive(Clone, uniffi::Object)]
pub struct TimelineItemContent(pub(crate) matrix_sdk_ui::timeline::TimelineItemContent);
#[uniffi::export]
impl TimelineItemContent {
pub fn kind(&self) -> TimelineItemContentKind {
impl From<&matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
fn from(value: &matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
match &self.0 {
Content::Message(_) => TimelineItemContentKind::Message,
match value {
Content::Message(message) => TimelineItemContent::Message { content: message.into() },
Content::RedactedMessage => TimelineItemContentKind::RedactedMessage,
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
Content::Sticker(sticker) => {
let content = sticker.content();
TimelineItemContentKind::Sticker {
TimelineItemContent::Sticker {
body: content.body.clone(),
info: (&content.info).into(),
source: Arc::new(MediaSource::from(content.source.clone())),
}
}
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
Content::Poll(poll_state) => TimelineItemContent::from(poll_state.results()),
Content::CallInvite => TimelineItemContentKind::CallInvite,
Content::CallInvite => TimelineItemContent::CallInvite,
Content::CallNotify => TimelineItemContentKind::CallNotify,
Content::CallNotify => TimelineItemContent::CallNotify,
Content::UnableToDecrypt(msg) => {
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
}
Content::MembershipChange(membership) => TimelineItemContentKind::RoomMembership {
Content::MembershipChange(membership) => TimelineItemContent::RoomMembership {
user_id: membership.user_id().to_string(),
user_display_name: membership.display_name(),
change: membership.change().map(Into::into),
@@ -74,7 +69,7 @@ impl TimelineItemContent {
)
})
.unzip();
TimelineItemContentKind::ProfileChange {
TimelineItemContent::ProfileChange {
display_name: display_name.flatten(),
prev_display_name: prev_display_name.flatten(),
avatar_url: avatar_url.flatten(),
@@ -82,20 +77,20 @@ impl TimelineItemContent {
}
}
Content::OtherState(state) => TimelineItemContentKind::State {
Content::OtherState(state) => TimelineItemContent::State {
state_key: state.state_key().to_owned(),
content: state.content().into(),
},
Content::FailedToParseMessageLike { event_type, error } => {
TimelineItemContentKind::FailedToParseMessageLike {
TimelineItemContent::FailedToParseMessageLike {
event_type: event_type.to_string(),
error: error.to_string(),
}
}
Content::FailedToParseState { event_type, state_key, error } => {
TimelineItemContentKind::FailedToParseState {
TimelineItemContent::FailedToParseState {
event_type: event_type.to_string(),
state_key: state_key.to_string(),
error: error.to_string(),
@@ -103,16 +98,45 @@ impl TimelineItemContent {
}
}
}
}
pub fn as_message(self: Arc<Self>) -> Option<Arc<Message>> {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
unwrap_or_clone_arc_into_variant!(self, .0, Content::Message(msg) => Arc::new(Message(msg)))
#[derive(Clone, uniffi::Record)]
pub struct MessageContent {
pub msg_type: MessageType,
pub body: String,
pub in_reply_to: Option<Arc<InReplyToDetails>>,
pub thread_root: Option<String>,
pub is_edited: bool,
pub mentions: Option<Mentions>,
}
impl From<&matrix_sdk_ui::timeline::Message> for MessageContent {
fn from(value: &matrix_sdk_ui::timeline::Message) -> Self {
Self {
msg_type: value.msgtype().clone().into(),
body: value.body().to_owned(),
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.into())),
is_edited: value.is_edited(),
thread_root: value.thread_root().map(|id| id.to_string()),
mentions: value.mentions().cloned().map(|m| m.into()),
}
}
}
#[derive(uniffi::Enum)]
pub enum TimelineItemContentKind {
Message,
impl From<ruma::events::Mentions> for Mentions {
fn from(value: ruma::events::Mentions) -> Self {
Self {
user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
room: value.room,
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum TimelineItemContent {
Message {
content: MessageContent,
},
RedactedMessage,
Sticker {
body: String,
@@ -160,36 +184,6 @@ pub enum TimelineItemContentKind {
}
#[derive(Clone, uniffi::Object)]
pub struct Message(matrix_sdk_ui::timeline::Message);
#[uniffi::export]
impl Message {
pub fn msgtype(&self) -> MessageType {
self.0.msgtype().clone().into()
}
pub fn body(&self) -> String {
self.0.msgtype().body().to_owned()
}
pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
self.0.in_reply_to().map(InReplyToDetails::from)
}
pub fn is_threaded(&self) -> bool {
self.0.is_threaded()
}
pub fn is_edited(&self) -> bool {
self.0.is_edited()
}
pub fn content(&self) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(self.0.msgtype().clone()))
}
}
#[derive(uniffi::Record)]
pub struct InReplyToDetails {
event_id: String,
event: RepliedToEventDetails,
@@ -201,6 +195,17 @@ impl InReplyToDetails {
}
}
#[uniffi::export]
impl InReplyToDetails {
pub fn event_id(&self) -> String {
self.event_id.clone()
}
pub fn event(&self) -> RepliedToEventDetails {
self.event.clone()
}
}
impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
let event_id = inner.event_id.to_string();
@@ -208,7 +213,7 @@ impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
TimelineDetails::Pending => RepliedToEventDetails::Pending,
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
content: Arc::new(TimelineItemContent(event.content().to_owned())),
content: event.content().into(),
sender: event.sender().to_string(),
sender_profile: event.sender_profile().into(),
},
@@ -221,11 +226,11 @@ impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
}
}
#[derive(uniffi::Enum)]
#[derive(Clone, uniffi::Enum)]
pub enum RepliedToEventDetails {
Unavailable,
Pending,
Ready { content: Arc<TimelineItemContent>, sender: String, sender_profile: ProfileDetails },
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
Error { message: String },
}
@@ -419,15 +424,15 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
}
}
#[derive(uniffi::Record)]
#[derive(Clone, uniffi::Record)]
pub struct PollAnswer {
pub id: String,
pub text: String,
}
impl From<PollResult> for TimelineItemContentKind {
impl From<PollResult> for TimelineItemContent {
fn from(value: PollResult) -> Self {
TimelineItemContentKind::Poll {
TimelineItemContent::Poll {
question: value.question,
kind: PollKind::from(value.kind),
max_selections: value.max_selections,
+105 -106
View File
@@ -78,6 +78,8 @@ use crate::{
mod content;
pub use content::MessageContent;
#[derive(uniffi::Object)]
#[repr(transparent)]
pub struct Timeline {
@@ -423,11 +425,11 @@ impl Timeline {
pub async fn send_poll_response(
self: Arc<Self>,
poll_start_id: String,
poll_start_event_id: String,
answers: Vec<String>,
) -> Result<(), ClientError> {
let poll_start_event_id =
EventId::parse(poll_start_id).context("Failed to parse EventId")?;
EventId::parse(poll_start_event_id).context("Failed to parse EventId")?;
let poll_response_event_content =
UnstablePollResponseEventContent::new(answers, poll_start_event_id);
let event_content =
@@ -442,11 +444,11 @@ impl Timeline {
pub fn end_poll(
self: Arc<Self>,
poll_start_id: String,
poll_start_event_id: String,
text: String,
) -> Result<(), ClientError> {
let poll_start_event_id =
EventId::parse(poll_start_id).context("Failed to parse EventId")?;
EventId::parse(poll_start_event_id).context("Failed to parse EventId")?;
let poll_end_event_content = UnstablePollEndEventContent::new(text, poll_start_event_id);
let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content);
@@ -486,13 +488,19 @@ impl Timeline {
///
/// Returns whether the edit did happen. It can only return false for
/// local events that are being processed.
pub async fn edit(
&self,
item: Arc<EventTimelineItem>,
new_content: EditedContent,
) -> Result<bool, ClientError> {
let new_content: SdkEditedContent = new_content.try_into()?;
self.inner.edit(&item.0, new_content).await.map_err(ClientError::from)
pub async fn edit(&self, id: String, new_content: EditedContent) -> Result<bool, ClientError> {
let event = if let Ok(event_id) = EventId::parse(&id) {
self.inner.item_by_event_id(&event_id).await
} else {
let transaction_id: OwnedTransactionId = id.into();
self.inner.local_item_by_transaction_id(&transaction_id).await
};
if let Some(event) = event {
let new_content: SdkEditedContent = new_content.try_into()?;
self.inner.edit(&event, new_content).await.map_err(ClientError::from)
} else {
Ok(false)
}
}
pub async fn send_location(
@@ -558,14 +566,14 @@ impl Timeline {
pub async fn get_event_timeline_item_by_event_id(
&self,
event_id: String,
) -> Result<Arc<EventTimelineItem>, ClientError> {
) -> Result<EventTimelineItem, ClientError> {
let event_id = EventId::parse(event_id)?;
let item = self
.inner
.item_by_event_id(&event_id)
.await
.context("Item with given event ID not found")?;
Ok(Arc::new(EventTimelineItem(item)))
Ok(item.into())
}
/// Get the current timeline item for the given transaction ID, if any.
@@ -578,14 +586,14 @@ impl Timeline {
pub async fn get_event_timeline_item_by_transaction_id(
&self,
transaction_id: String,
) -> Result<Arc<EventTimelineItem>, ClientError> {
) -> Result<EventTimelineItem, ClientError> {
let transaction_id: OwnedTransactionId = transaction_id.into();
let item = self
.inner
.local_item_by_transaction_id(&transaction_id)
.await
.context("Item with given transaction ID not found")?;
Ok(Arc::new(EventTimelineItem(item)))
Ok(item.into())
}
/// Redacts an event from the timeline.
@@ -600,16 +608,26 @@ impl Timeline {
/// local events that are being processed.
pub async fn redact_event(
&self,
item: Arc<EventTimelineItem>,
id: String,
reason: Option<String>,
) -> Result<bool, ClientError> {
let removed = self
.inner
.redact(&item.0, reason.as_deref())
.await
.map_err(|err| anyhow::anyhow!(err))?;
let event = if let Ok(event_id) = EventId::parse(&id) {
self.inner.item_by_event_id(&event_id).await
} else {
let transaction_id: OwnedTransactionId = id.into();
self.inner.local_item_by_transaction_id(&transaction_id).await
};
if let Some(event) = event {
let removed = self
.inner
.redact(&event, reason.as_deref())
.await
.map_err(|err| anyhow::anyhow!(err))?;
Ok(removed)
Ok(removed)
} else {
Ok(false)
}
}
/// Load the reply details for the given event id.
@@ -640,7 +658,7 @@ impl Timeline {
Ok(replied_to) => Ok(InReplyToDetails::new(
event_id_str,
RepliedToEventDetails::Ready {
content: Arc::new(TimelineItemContent(replied_to.content().clone())),
content: replied_to.content().into(),
sender: replied_to.sender().to_string(),
sender_profile: replied_to.sender_profile().into(),
},
@@ -672,6 +690,14 @@ impl Timeline {
let event_id = EventId::parse(event_id).map_err(ClientError::from)?;
self.inner.unpin_event(&event_id).await.map_err(ClientError::from)
}
pub fn create_message_content(
&self,
msg_type: crate::ruma::MessageType,
) -> Option<Arc<RoomMessageEventContentWithoutRelation>> {
let msg_type: Option<MessageType> = msg_type.try_into().ok();
msg_type.map(|m| Arc::new(RoomMessageEventContentWithoutRelation::new(m)))
}
}
#[derive(uniffi::Object)]
@@ -871,9 +897,9 @@ impl TimelineItem {
#[uniffi::export]
impl TimelineItem {
pub fn as_event(self: Arc<Self>) -> Option<Arc<EventTimelineItem>> {
pub fn as_event(self: Arc<Self>) -> Option<EventTimelineItem> {
let event_item = self.0.as_event()?;
Some(Arc::new(EventTimelineItem(event_item.clone())))
Some(event_item.clone().into())
}
pub fn as_virtual(self: Arc<Self>) -> Option<VirtualTimelineItem> {
@@ -995,7 +1021,7 @@ fn event_send_state_from_sending_failed(error: &Error, is_recoverable: bool) ->
/// Recommended decorations for decrypted messages, representing the message's
/// authenticity properties.
#[derive(uniffi::Enum)]
#[derive(uniffi::Enum, Clone)]
pub enum ShieldState {
/// A red shield with a tooltip containing the associated message should be
/// presented.
@@ -1021,100 +1047,73 @@ impl From<SdkShieldState> for ShieldState {
}
}
#[derive(uniffi::Object)]
pub struct EventTimelineItem(pub(crate) matrix_sdk_ui::timeline::EventTimelineItem);
#[derive(Clone, uniffi::Record)]
pub struct EventTimelineItem {
is_local: bool,
is_remote: bool,
transaction_id: Option<String>,
event_id: Option<String>,
sender: String,
sender_profile: ProfileDetails,
is_own: bool,
is_editable: bool,
content: TimelineItemContent,
timestamp: u64,
reactions: Vec<Reaction>,
debug_info: EventTimelineItemDebugInfo,
local_send_state: Option<EventSendState>,
read_receipts: HashMap<String, Receipt>,
origin: Option<EventItemOrigin>,
can_be_replied_to: bool,
message_shield: Option<ShieldState>,
}
#[uniffi::export]
impl EventTimelineItem {
pub fn is_local(&self) -> bool {
self.0.is_local_echo()
}
pub fn is_remote(&self) -> bool {
!self.0.is_local_echo()
}
pub fn transaction_id(&self) -> Option<String> {
self.0.transaction_id().map(ToString::to_string)
}
pub fn event_id(&self) -> Option<String> {
self.0.event_id().map(ToString::to_string)
}
pub fn sender(&self) -> String {
self.0.sender().to_string()
}
pub fn sender_profile(&self) -> ProfileDetails {
self.0.sender_profile().into()
}
pub fn is_own(&self) -> bool {
self.0.is_own()
}
pub fn is_editable(&self) -> bool {
self.0.is_editable()
}
pub fn content(&self) -> Arc<TimelineItemContent> {
Arc::new(TimelineItemContent(self.0.content().clone()))
}
pub fn timestamp(&self) -> u64 {
self.0.timestamp().0.into()
}
pub fn reactions(&self) -> Vec<Reaction> {
self.0
impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
fn from(value: matrix_sdk_ui::timeline::EventTimelineItem) -> Self {
let reactions = value
.reactions()
.iter()
.map(|(k, v)| Reaction {
key: k.to_owned(),
senders: v
.iter()
.into_iter()
.map(|(sender_id, info)| ReactionSenderData {
sender_id: sender_id.to_string(),
timestamp: info.timestamp.0.into(),
})
.collect(),
})
.collect()
}
pub fn debug_info(&self) -> EventTimelineItemDebugInfo {
EventTimelineItemDebugInfo {
model: format!("{:#?}", self.0),
original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()),
latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()),
.collect();
let debug_info = EventTimelineItemDebugInfo {
model: format!("{:#?}", value),
original_json: value.original_json().map(|raw| raw.json().get().to_owned()),
latest_edit_json: value.latest_edit_json().map(|raw| raw.json().get().to_owned()),
};
let read_receipts =
value.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect();
Self {
is_local: value.is_local_echo(),
is_remote: !value.is_local_echo(),
transaction_id: value.transaction_id().map(|t| t.to_string()),
event_id: value.event_id().map(|e| e.to_string()),
sender: value.sender().to_string(),
sender_profile: value.sender_profile().into(),
is_own: value.is_own(),
is_editable: value.is_editable(),
content: value.content().into(),
timestamp: value.timestamp().0.into(),
reactions,
debug_info,
local_send_state: value.send_state().map(|s| s.into()),
read_receipts,
origin: value.origin(),
can_be_replied_to: value.can_be_replied_to(),
message_shield: value.get_shield(false).map(Into::into),
}
}
pub fn local_send_state(&self) -> Option<EventSendState> {
self.0.send_state().map(Into::into)
}
pub fn read_receipts(&self) -> HashMap<String, Receipt> {
self.0.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect()
}
pub fn origin(&self) -> Option<EventItemOrigin> {
self.0.origin()
}
pub fn can_be_replied_to(&self) -> bool {
self.0.can_be_replied_to()
}
/// Gets the [`ShieldState`] which can be used to decorate messages in the
/// recommended way.
pub fn get_shield(&self, strict: bool) -> Option<ShieldState> {
self.0.get_shield(strict).map(Into::into)
}
}
#[derive(uniffi::Record)]
#[derive(Clone, uniffi::Record)]
pub struct Receipt {
pub timestamp: Option<u64>,
}
@@ -1125,14 +1124,14 @@ impl From<ruma::events::receipt::Receipt> for Receipt {
}
}
#[derive(uniffi::Record)]
#[derive(Clone, uniffi::Record)]
pub struct EventTimelineItemDebugInfo {
model: String,
original_json: Option<String>,
latest_edit_json: Option<String>,
}
#[derive(uniffi::Enum)]
#[derive(Clone, uniffi::Enum)]
pub enum ProfileDetails {
Unavailable,
Pending,
@@ -117,6 +117,11 @@ impl Message {
Self { msgtype, in_reply_to, thread_root, edited, mentions }
}
/// Create a forwarded message from a [`MessageType`].
pub fn forwarded(msgtype: MessageType) -> Self {
Self { msgtype, in_reply_to: None, thread_root: None, edited: false, mentions: None }
}
/// Get the `msgtype`-specific data of this message.
pub fn msgtype(&self) -> &MessageType {
&self.msgtype
@@ -139,6 +144,11 @@ impl Message {
self.thread_root.is_some()
}
/// Get the [`OwnedEventId`] of the root event of a thread if it exists.
pub fn thread_root(&self) -> Option<OwnedEventId> {
self.thread_root.clone()
}
/// Get the edit state of this message (has been edited: `true` /
/// `false`).
pub fn is_edited(&self) -> bool {