Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 81 additions & 5 deletions bindings/mobile/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::sync::Arc;

use chrono::{DateTime, Utc};
use prost::Message;
use xmtp_content_types::{
actions::{Action, ActionStyle, Actions},
attachment::Attachment,
Expand All @@ -22,8 +23,8 @@ use xmtp_proto::xmtp::mls::message_contents::{
};
use xmtp_proto::xmtp::mls::message_contents::{
content_types::{
DeleteMessage, LeaveRequest, MultiRemoteAttachment, ReactionAction, ReactionSchema,
ReactionV2,
DeleteMessage, EditMessage, LeaveRequest, MultiRemoteAttachment, ReactionAction,
ReactionSchema, ReactionV2,
},
group_updated::Inbox,
};
Expand Down Expand Up @@ -286,6 +287,12 @@ pub struct FfiDeleteMessage {
pub message_id: String,
}

#[derive(uniffi::Record, Clone, Debug)]
pub struct FfiEditMessage {
pub message_id: String,
pub edited_content: Option<FfiEncodedContent>,
}

#[derive(uniffi::Record, Clone, Debug)]
pub struct FfiWalletSendCalls {
pub version: String,
Expand Down Expand Up @@ -726,6 +733,24 @@ impl From<FfiDeleteMessage> for DeleteMessage {
}
}

impl From<EditMessage> for FfiEditMessage {
fn from(value: EditMessage) -> Self {
FfiEditMessage {
message_id: value.message_id,
edited_content: value.edited_content.map(Into::into),
}
}
}

impl From<FfiEditMessage> for EditMessage {
fn from(value: FfiEditMessage) -> Self {
EditMessage {
message_id: value.message_id,
edited_content: value.edited_content.map(Into::into),
}
}
}

impl From<WalletSendCalls> for FfiWalletSendCalls {
fn from(value: WalletSendCalls) -> Self {
FfiWalletSendCalls {
Expand Down Expand Up @@ -1201,6 +1226,7 @@ pub struct FfiDecodedMessage {
num_replies: u64,
inserted_at_ns: i64,
expires_at_ns: Option<i64>,
edited_at_ns: Option<i64>,
}

#[uniffi::export]
Expand Down Expand Up @@ -1270,6 +1296,14 @@ impl FfiDecodedMessage {
pub fn expires_at_ns(&self) -> Option<i64> {
self.expires_at_ns
}

pub fn edited_at_ns(&self) -> Option<i64> {
self.edited_at_ns
}

pub fn is_edited(&self) -> bool {
self.edited_at_ns.is_some()
}
}

impl From<DecodedMessage> for FfiDecodedMessage {
Expand All @@ -1278,17 +1312,58 @@ impl From<DecodedMessage> for FfiDecodedMessage {
// Extract metadata fields directly, consuming the metadata
let metadata: FfiDecodedMessageMetadata = item.metadata.into();

let edited_at_ns = item.edited.as_ref().map(|e| e.edited_at_ns);

let (content, content_type) = if let Some(edited) = item.edited {
match EncodedContent::decode(&mut edited.content.as_slice()) {
Ok(encoded_content) => {
let edited_content_type = encoded_content
.r#type
.clone()
.map(|ct| ct.into())
.unwrap_or(metadata.content_type.clone());
match MessageBody::try_from(encoded_content) {
Ok(mut edited_body) => {
let mut final_content_type = edited_content_type;

// Preserve in_reply_to from the original Reply
if let (
MessageBody::Reply(original_reply),
MessageBody::Reply(edited_reply),
) = (&item.content, &mut edited_body)
{
edited_reply.in_reply_to = original_reply.in_reply_to.clone();
}
// Wrap non-Reply edited content in Reply if original was a Reply
else if let MessageBody::Reply(original_reply) = &item.content {
edited_body = MessageBody::Reply(ProcessedReply {
in_reply_to: original_reply.in_reply_to.clone(),
content: Box::new(edited_body),
reference_id: original_reply.reference_id.clone(),
});
final_content_type = metadata.content_type.clone();
}
(edited_body.into(), final_content_type)
}
Err(_) => (item.content.into(), metadata.content_type.clone()),
}
}
Err(_) => (item.content.into(), metadata.content_type.clone()),
}
} else {
(item.content.into(), metadata.content_type.clone())
};

FfiDecodedMessage {
// Take ownership of all the data - no clones!
id: metadata.id,
sent_at_ns: metadata.sent_at_ns,
kind: metadata.kind,
conversation_id: metadata.conversation_id,
sender_installation_id: metadata.sender_installation_id,
sender_inbox_id: metadata.sender_inbox_id,
delivery_status,
content_type: metadata.content_type,
content: item.content.into(),
content_type,
content,
fallback_text: item.fallback_text,
reactions: item
.reactions
Expand All @@ -1299,6 +1374,7 @@ impl From<DecodedMessage> for FfiDecodedMessage {
num_replies: item.num_replies as u64,
inserted_at_ns: metadata.inserted_at_ns,
expires_at_ns: metadata.expires_at_ns,
edited_at_ns,
}
}
}
71 changes: 67 additions & 4 deletions bindings/mobile/src/mls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use xmtp_content_types::actions::{Actions, ActionsCodec};
use xmtp_content_types::attachment::Attachment;
use xmtp_content_types::attachment::AttachmentCodec;
use xmtp_content_types::delete_message::DeleteMessageCodec;
use xmtp_content_types::edit_message::EditMessageCodec;
use xmtp_content_types::group_updated::GroupUpdatedCodec;
use xmtp_content_types::intent::{Intent, IntentCodec};
use xmtp_content_types::leave_request::LeaveRequestCodec;
Expand Down Expand Up @@ -97,13 +98,14 @@ use xmtp_proto::types::Cursor;
use xmtp_proto::types::{ApiIdentifier, GroupMessageMetadata};
use xmtp_proto::xmtp::mls::message_contents::EncodedContent;
use xmtp_proto::xmtp::mls::message_contents::content_types::DeleteMessage;
use xmtp_proto::xmtp::mls::message_contents::content_types::EditMessage;
use xmtp_proto::xmtp::mls::message_contents::content_types::LeaveRequest;
use xmtp_proto::xmtp::mls::message_contents::content_types::{MultiRemoteAttachment, ReactionV2};

// Re-export types from message module that are used in public APIs
pub use crate::message::{
FfiAttachment, FfiDeleteMessage, FfiLeaveRequest, FfiMultiRemoteAttachment, FfiReadReceipt,
FfiRemoteAttachment, FfiTransactionReference,
FfiAttachment, FfiDeleteMessage, FfiEditMessage, FfiLeaveRequest, FfiMultiRemoteAttachment,
FfiReadReceipt, FfiRemoteAttachment, FfiTransactionReference,
};

pub mod device_sync;
Expand Down Expand Up @@ -1829,6 +1831,28 @@ impl FfiConversations {
FfiStreamCloser::new(handle)
}

/// Get notified when a message is edited.
/// The callback receives the decoded message with the edit info populated.
pub async fn stream_message_edits(
&self,
callback: Arc<dyn FfiMessageEditCallback>,
) -> FfiStreamCloser {
let error_callback = callback.clone();
let handle = RustXmtpClient::stream_message_edits_with_callback(
self.inner_client.clone(),
move |msg| match msg {
Ok(message) => {
let ffi_message: FfiDecodedMessage = message.into();
callback.on_message_edited(Arc::new(ffi_message))
}
Err(e) => error_callback.on_error(e.into()),
},
|| {},
);

FfiStreamCloser::new(handle)
}

pub fn get_hmac_keys(&self) -> Result<HashMap<Vec<u8>, Vec<FfiHmacKey>>, FfiError> {
let inner = self.inner_client.as_ref();
let conversations = inner.find_groups(GroupQueryArgs {
Expand Down Expand Up @@ -2345,6 +2369,16 @@ impl FfiConversation {
Ok(deletion_id)
}

/// Edit a message by its ID. Returns the ID of the edit message.
pub fn edit_message(
&self,
message_id: Vec<u8>,
new_content: Vec<u8>,
) -> Result<Vec<u8>, FfiError> {
let edit_id = self.inner.edit_message(message_id, new_content)?;
Ok(edit_id)
}

/// Publish all unpublished messages
pub async fn publish_messages(&self) -> Result<(), FfiError> {
self.inner.publish_messages().await?;
Expand Down Expand Up @@ -3126,7 +3160,6 @@ pub fn decode_leave_request(bytes: Vec<u8>) -> Result<FfiLeaveRequest, FfiError>
.map_err(|e| FfiError::generic(e.to_string()))
}

// DeleteMessage FFI encode function
#[uniffi::export]
pub fn encode_delete_message(request: FfiDeleteMessage) -> Result<Vec<u8>, FfiError> {
let delete_message: DeleteMessage = request.into();
Expand All @@ -3142,7 +3175,6 @@ pub fn encode_delete_message(request: FfiDeleteMessage) -> Result<Vec<u8>, FfiEr
Ok(buf)
}

// DeleteMessage FFI decode function
#[uniffi::export]
pub fn decode_delete_message(bytes: Vec<u8>) -> Result<FfiDeleteMessage, FfiError> {
let encoded_content =
Expand All @@ -3153,6 +3185,31 @@ pub fn decode_delete_message(bytes: Vec<u8>) -> Result<FfiDeleteMessage, FfiErro
.map_err(|e| FfiError::generic(e.to_string()))
}

#[uniffi::export]
pub fn encode_edit_message(request: FfiEditMessage) -> Result<Vec<u8>, FfiError> {
let edit_message: EditMessage = request.into();

let encoded =
EditMessageCodec::encode(edit_message).map_err(|e| FfiError::generic(e.to_string()))?;

let mut buf = Vec::new();
encoded
.encode(&mut buf)
.map_err(|e| FfiError::generic(e.to_string()))?;

Ok(buf)
}

#[uniffi::export]
pub fn decode_edit_message(bytes: Vec<u8>) -> Result<FfiEditMessage, FfiError> {
let encoded_content =
EncodedContent::decode(bytes.as_slice()).map_err(|e| FfiError::generic(e.to_string()))?;

EditMessageCodec::decode(encoded_content)
.map(Into::into)
.map_err(|e| FfiError::generic(e.to_string()))
}

#[uniffi::export]
pub fn decode_group_updated(bytes: Vec<u8>) -> Result<FfiGroupUpdated, FfiError> {
let encoded_content =
Expand Down Expand Up @@ -3426,6 +3483,12 @@ pub trait FfiMessageDeletionCallback: Send + Sync {
fn on_message_deleted(&self, message: Arc<FfiDecodedMessage>);
}

#[uniffi::export(with_foreign)]
pub trait FfiMessageEditCallback: Send + Sync {
fn on_message_edited(&self, message: Arc<FfiDecodedMessage>);
fn on_error(&self, error: FfiError);
}

#[derive(uniffi::Enum, Debug)]
pub enum FfiPreferenceUpdate {
HMAC { key: Vec<u8> },
Expand Down
Loading
Loading