Skip to content

Commit

Permalink
Conversation list last message filters for readable content types (#1518
Browse files Browse the repository at this point in the history
)

* conversation list last message filters for readable content types

* ignore clippy in test loop

* remove outdated test

* fix existing tests

* fix another test

---------

Co-authored-by: cameronvoell <[email protected]>
  • Loading branch information
cameronvoell and cameronvoell authored Jan 17, 2025
1 parent 191b8f3 commit fc37819
Show file tree
Hide file tree
Showing 5 changed files with 368 additions and 48 deletions.
283 changes: 238 additions & 45 deletions bindings_ffi/src/mls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2252,7 +2252,13 @@ mod tests {
use tokio::{sync::Notify, time::error::Elapsed};
use xmtp_common::tmp_path;
use xmtp_common::{wait_for_eq, wait_for_ok};
use xmtp_content_types::{read_receipt, text::TextCodec, ContentCodec};
use xmtp_content_types::{
attachment::AttachmentCodec, bytes_to_encoded_content, encoded_content_to_bytes,
group_updated::GroupUpdatedCodec, membership_change::GroupMembershipChangeCodec,
reaction::ReactionCodec, read_receipt::ReadReceiptCodec,
remote_attachment::RemoteAttachmentCodec, reply::ReplyCodec, text::TextCodec,
transaction_reference::TransactionReferenceCodec, ContentCodec,
};
use xmtp_cryptography::{signature::RecoverableSignature, utils::rng};
use xmtp_id::associations::{
generate_inbox_id,
Expand Down Expand Up @@ -3056,12 +3062,14 @@ mod tests {
.unwrap();

// Add messages to the group
let text_message_1 = TextCodec::encode("Text message for Group 1".to_string()).unwrap();
group
.send("First message".as_bytes().to_vec())
.send(encoded_content_to_bytes(text_message_1))
.await
.unwrap();
let text_message_2 = TextCodec::encode("Text message for Group 2".to_string()).unwrap();
group
.send("Second message".as_bytes().to_vec())
.send(encoded_content_to_bytes(text_message_2))
.await
.unwrap();

Expand All @@ -3081,8 +3089,8 @@ mod tests {

let last_message = conversations[0].last_message.as_ref().unwrap();
assert_eq!(
last_message.content,
"Second message".as_bytes().to_vec(),
TextCodec::decode(bytes_to_encoded_content(last_message.content.clone())).unwrap(),
"Text message for Group 2".to_string(),
"Last message content should be the most recent"
);
}
Expand Down Expand Up @@ -3126,85 +3134,270 @@ mod tests {
}

#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn test_conversation_list_ordering() {
async fn test_conversation_list_filters_readable_messages() {
// Step 1: Setup test client
let client = new_test_client().await;
let conversations_api = client.conversations();

// Step 2: Create Group A
let group_a = conversations_api
.create_group(vec![], FfiCreateGroupOptions::default())
// Step 2: Create 9 groups
let mut groups = Vec::with_capacity(9);
for _ in 0..9 {
let group = conversations_api
.create_group(vec![], FfiCreateGroupOptions::default())
.await
.unwrap();
groups.push(group);
}

// Step 3: Each group gets a message sent in it by type following the pattern:
// group[0] -> TextCodec (readable)
// group[1] -> ReactionCodec (readable)
// group[2] -> AttachmentCodec (readable)
// group[3] -> RemoteAttachmentCodec (readable)
// group[4] -> ReplyCodec (readable)
// group[5] -> TransactionReferenceCodec (readable)
// group[6] -> GroupUpdatedCodec (not readable)
// group[7] -> GroupMembershipUpdatedCodec (not readable)
// group[8] -> ReadReceiptCodec (not readable)

// group[0] sends TextCodec message
let text_message = TextCodec::encode("Text message for Group 1".to_string()).unwrap();
groups[0]
.send(encoded_content_to_bytes(text_message))
.await
.unwrap();

// group[1] sends ReactionCodec message
let reaction_content_type_id = ContentTypeId {
authority_id: "".to_string(),
type_id: ReactionCodec::TYPE_ID.to_string(),
version_major: 0,
version_minor: 0,
};
let reaction_encoded_content = EncodedContent {
r#type: Some(reaction_content_type_id),
content: "reaction content".as_bytes().to_vec(),
parameters: HashMap::new(),
fallback: None,
compression: None,
};
groups[1]
.send(encoded_content_to_bytes(reaction_encoded_content))
.await
.unwrap();

// group[2] sends AttachmentCodec message
let attachment_content_type_id = ContentTypeId {
authority_id: "".to_string(),
type_id: AttachmentCodec::TYPE_ID.to_string(),
version_major: 0,
version_minor: 0,
};
let attachment_encoded_content = EncodedContent {
r#type: Some(attachment_content_type_id),
content: "attachment content".as_bytes().to_vec(),
parameters: HashMap::new(),
fallback: None,
compression: None,
};
groups[2]
.send(encoded_content_to_bytes(attachment_encoded_content))
.await
.unwrap();

// group[3] sends RemoteAttachmentCodec message
let remote_attachment_content_type_id = ContentTypeId {
authority_id: "".to_string(),
type_id: RemoteAttachmentCodec::TYPE_ID.to_string(),
version_major: 0,
version_minor: 0,
};
let remote_attachment_encoded_content = EncodedContent {
r#type: Some(remote_attachment_content_type_id),
content: "remote attachment content".as_bytes().to_vec(),
parameters: HashMap::new(),
fallback: None,
compression: None,
};
groups[3]
.send(encoded_content_to_bytes(remote_attachment_encoded_content))
.await
.unwrap();

// group[4] sends ReplyCodec message
let reply_content_type_id = ContentTypeId {
authority_id: "".to_string(),
type_id: ReplyCodec::TYPE_ID.to_string(),
version_major: 0,
version_minor: 0,
};
let reply_encoded_content = EncodedContent {
r#type: Some(reply_content_type_id),
content: "reply content".as_bytes().to_vec(),
parameters: HashMap::new(),
fallback: None,
compression: None,
};
groups[4]
.send(encoded_content_to_bytes(reply_encoded_content))
.await
.unwrap();

// group[5] sends TransactionReferenceCodec message
let transaction_reference_content_type_id = ContentTypeId {
authority_id: "".to_string(),
type_id: TransactionReferenceCodec::TYPE_ID.to_string(),
version_major: 0,
version_minor: 0,
};
let transaction_reference_encoded_content = EncodedContent {
r#type: Some(transaction_reference_content_type_id),
content: "transaction reference".as_bytes().to_vec(),
parameters: HashMap::new(),
fallback: None,
compression: None,
};
groups[5]
.send(encoded_content_to_bytes(
transaction_reference_encoded_content,
))
.await
.unwrap();

// Step 3: Create Group B
let group_b = conversations_api
.create_group(vec![], FfiCreateGroupOptions::default())
// group[6] sends GroupUpdatedCodec message
let group_updated_content_type_id = ContentTypeId {
authority_id: "".to_string(),
type_id: GroupUpdatedCodec::TYPE_ID.to_string(),
version_major: 0,
version_minor: 0,
};
let group_updated_encoded_content = EncodedContent {
r#type: Some(group_updated_content_type_id),
content: "group updated content".as_bytes().to_vec(),
parameters: HashMap::new(),
fallback: None,
compression: None,
};
groups[6]
.send(encoded_content_to_bytes(group_updated_encoded_content))
.await
.unwrap();

// Step 4: Send a message to Group A
group_a
.send("Message to Group A".as_bytes().to_vec())
// group[7] sends GroupMembershipUpdatedCodec message
let group_membership_updated_content_type_id = ContentTypeId {
authority_id: "".to_string(),
type_id: GroupMembershipChangeCodec::TYPE_ID.to_string(),
version_major: 0,
version_minor: 0,
};
let group_membership_updated_encoded_content = EncodedContent {
r#type: Some(group_membership_updated_content_type_id),
content: "group membership updated".as_bytes().to_vec(),
parameters: HashMap::new(),
fallback: None,
compression: None,
};
groups[7]
.send(encoded_content_to_bytes(
group_membership_updated_encoded_content,
))
.await
.unwrap();

// Step 5: Create Group C
let group_c = conversations_api
.create_group(vec![], FfiCreateGroupOptions::default())
// group[8] sends ReadReceiptCodec message
let read_receipt_content_type_id = ContentTypeId {
authority_id: "".to_string(),
type_id: ReadReceiptCodec::TYPE_ID.to_string(),
version_major: 0,
version_minor: 0,
};
let read_receipt_encoded_content = EncodedContent {
r#type: Some(read_receipt_content_type_id),
content: "read receipt content".as_bytes().to_vec(),
parameters: HashMap::new(),
fallback: None,
compression: None,
};
groups[8]
.send(encoded_content_to_bytes(read_receipt_encoded_content))
.await
.unwrap();

// Step 6: Synchronize conversations
// Step 4: Synchronize all conversations
conversations_api
.sync_all_conversations(None)
.await
.unwrap();

// Step 7: Fetch the conversation list
// Step 5: Fetch the list of conversations
let conversations = conversations_api
.list(FfiListConversationsOptions::default())
.unwrap();

// Step 8: Assert the correct order of conversations
// Step 6: Verify the order of conversations by last readable message sent (or recently created if no readable message)
// The order should be: 5, 4, 3, 2, 1, 0, 8, 7, 6
assert_eq!(
conversations.len(),
3,
"There should be exactly 3 conversations"
9,
"There should be exactly 9 conversations"
);

// Verify the order: Group C, Group A, Group B
assert_eq!(
conversations[0].conversation.inner.group_id, group_c.inner.group_id,
"Group C should be the first conversation"
conversations[0].conversation.inner.group_id, groups[5].inner.group_id,
"Group 6 should be the first conversation"
);
assert_eq!(
conversations[1].conversation.inner.group_id, group_a.inner.group_id,
"Group A should be the second conversation"
conversations[1].conversation.inner.group_id, groups[4].inner.group_id,
"Group 5 should be the second conversation"
);
assert_eq!(
conversations[2].conversation.inner.group_id, group_b.inner.group_id,
"Group B should be the third conversation"
conversations[2].conversation.inner.group_id, groups[3].inner.group_id,
"Group 4 should be the third conversation"
);

// Verify the last_message field for Group A and None for others
assert!(
conversations[0].last_message.is_none(),
"Group C should have no messages"
assert_eq!(
conversations[3].conversation.inner.group_id, groups[2].inner.group_id,
"Group 3 should be the fourth conversation"
);
assert!(
conversations[1].last_message.is_some(),
"Group A should have a last message"
assert_eq!(
conversations[4].conversation.inner.group_id, groups[1].inner.group_id,
"Group 2 should be the fifth conversation"
);
assert_eq!(
conversations[1].last_message.as_ref().unwrap().content,
"Message to Group A".as_bytes().to_vec(),
"Group A's last message content should match"
conversations[5].conversation.inner.group_id, groups[0].inner.group_id,
"Group 1 should be the sixth conversation"
);
assert!(
conversations[2].last_message.is_none(),
"Group B should have no messages"
assert_eq!(
conversations[6].conversation.inner.group_id, groups[8].inner.group_id,
"Group 9 should be the seventh conversation"
);
assert_eq!(
conversations[7].conversation.inner.group_id, groups[7].inner.group_id,
"Group 8 should be the eighth conversation"
);
assert_eq!(
conversations[8].conversation.inner.group_id, groups[6].inner.group_id,
"Group 7 should be the ninth conversation"
);

// Step 7: Verify that for conversations 0 through 5, last_message is Some
// Index of group[0] in conversations -> 5
for i in 0..=5 {
assert!(
conversations[5 - i].last_message.is_some(),
"Group {} should have a last message",
i + 1
);
}

// Step 8: Verify that for conversations 6, 7, 8, last_message is None
#[allow(clippy::needless_range_loop)]
for i in 6..=8 {
assert!(
conversations[i].last_message.is_none(),
"Group {} should have no last message",
i + 1
);
}
}

#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
Expand Down Expand Up @@ -5543,7 +5736,7 @@ mod tests {
// Bo sends read receipt
let read_receipt_content_id = ContentTypeId {
authority_id: "xmtp.org".to_string(),
type_id: read_receipt::ReadReceiptCodec::TYPE_ID.to_string(),
type_id: ReadReceiptCodec::TYPE_ID.to_string(),
version_major: 1,
version_minor: 0,
};
Expand Down
11 changes: 11 additions & 0 deletions xmtp_content_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod reply;
pub mod text;
pub mod transaction_reference;

use prost::Message;
use thiserror::Error;
use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent};

Expand All @@ -24,3 +25,13 @@ pub trait ContentCodec<T> {
fn encode(content: T) -> Result<EncodedContent, CodecError>;
fn decode(content: EncodedContent) -> Result<T, CodecError>;
}

pub fn encoded_content_to_bytes(content: EncodedContent) -> Vec<u8> {
let mut buf = Vec::new();
content.encode(&mut buf).unwrap();
buf
}

pub fn bytes_to_encoded_content(bytes: Vec<u8>) -> EncodedContent {
EncodedContent::decode(&mut bytes.as_slice()).unwrap()
}
Loading

0 comments on commit fc37819

Please sign in to comment.