Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
mchenani committed Dec 19, 2024
1 parent b5f3237 commit 03ba962
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 3 deletions.
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions bindings_ffi/src/mls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,20 @@ impl FfiConversations {
Ok(convo_list)
}

pub async fn list_conversations(
&self,
opts: FfiListConversationsOptions,
) -> Result<Vec<Arc<FfiConversationListItem>>, GenericError> {
let inner = self.inner_client.as_ref();
let convo_list: Vec<Arc<FfiConversationListItem>> = inner
.list_conversations()?
.into_iter()
.map(|group| Arc::new(group.into()))
.collect();

Ok(convo_list)
}

pub async fn list_groups(
&self,
opts: FfiListConversationsOptions,
Expand Down Expand Up @@ -1149,6 +1163,12 @@ pub struct FfiConversation {
inner: MlsGroup<RustXmtpClient>,
}

#[derive(uniffi::Object)]
pub struct FfiConversationListItem {
conversation: FfiConversation,
last_message: FfiMessage
}

impl From<MlsGroup<RustXmtpClient>> for FfiConversation {
fn from(mls_group: MlsGroup<RustXmtpClient>) -> FfiConversation {
FfiConversation { inner: mls_group }
Expand Down
26 changes: 26 additions & 0 deletions xmtp_mls/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,32 @@ where
.collect())
}

pub fn list_conversations(
&self,
) -> Result<Vec<(MlsGroup<Self>, StoredGroupMessage)>, ClientError> {
Ok(self
.store()
.conn()?
.fetch_conversation_list()?
.into_iter()
.map(|conversation_item| {
(
StoredGroupMessage {
id: conversation_item.message_id?,
group_id: conversation_item.id.clone(),
decrypted_message_bytes: conversation_item.decrypted_message_bytes?,
sent_at_ns: conversation_item.sent_at_ns?,
sender_installation_id: conversation_item.sender_installation_id?,
sender_inbox_id: conversation_item.sender_inbox_id?,
kind: conversation_item.kind?,
delivery_status: conversation_item.delivery_status?,
},
MlsGroup::new(self.clone(), conversation_item.id, conversation_item.created_at_ns),
)
})
.collect())
}

/// Upload a Key Package to the network and publish the signed identity update
/// from the provided SignatureRequest
pub async fn register_identity(
Expand Down
2 changes: 2 additions & 0 deletions xmtp_mls/src/groups/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ impl RetryableError for GroupError {
pub struct MlsGroup<C> {
pub group_id: Vec<u8>,
pub created_at_ns: i64,
pub last_message: Option<StoredGroupMessage>,
pub client: Arc<C>,
mutex: Arc<Mutex<()>>,
}
Expand All @@ -289,6 +290,7 @@ impl<C> Clone for MlsGroup<C> {
Self {
group_id: self.group_id.clone(),
created_at_ns: self.created_at_ns,
last_message: self.last_message.clone(),
client: self.client.clone(),
mutex: self.mutex.clone(),
}
Expand Down
179 changes: 179 additions & 0 deletions xmtp_mls/src/storage/encrypted_store/conversation_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use crate::storage::group::{ConversationType, GroupMembershipState};
use crate::storage::group_message::{DeliveryStatus, GroupMessageKind};
use crate::storage::schema::conversation_list::dsl::conversation_list;
use crate::storage::{DbConnection, StorageError};
use crate::{Fetch, FetchListWithKey};
use diesel::{Identifiable, QueryDsl, Queryable, RunQueryDsl, Table};
use serde::{Deserialize, Serialize};

#[derive(Queryable, Debug, Clone, Deserialize, Serialize)]
#[diesel(table_name = conversation_list)]
#[diesel(primary_key(id))]
/// Combined view of a group and its messages, now named `conversation_list`.
pub struct ConversationListItem {
/// group_id
pub id: Vec<u8>,
/// Based on timestamp of the welcome message
pub created_at_ns: i64,
/// Enum, [`GroupMembershipState`] representing access to the group
pub membership_state: GroupMembershipState,
/// Track when the latest, most recent installations were checked
pub installations_last_checked: i64,
/// The inbox_id of who added the user to the group
pub added_by_inbox_id: String,
/// The sequence id of the welcome message
pub welcome_id: Option<i64>,
/// The inbox_id of the DM target
pub dm_inbox_id: Option<String>,
/// The last time the leaf node encryption key was rotated
pub rotated_at_ns: i64,
/// Enum, [`ConversationType`] signifies the group conversation type which extends to who can access it.
pub conversation_type: ConversationType,
/// Id of the message. Nullable because not every group has messages.
pub message_id: Option<Vec<u8>>,
/// Contents of message after decryption.
pub decrypted_message_bytes: Option<Vec<u8>>,
/// Time in nanoseconds the message was sent.
pub sent_at_ns: Option<i64>,
/// Group Message Kind Enum: 1 = Application, 2 = MembershipChange
pub kind: Option<GroupMessageKind>,
/// The ID of the App Installation this message was sent from.
pub sender_installation_id: Option<Vec<u8>>,
/// The Inbox ID of the Sender
pub sender_inbox_id: Option<String>,
/// We optimistically store messages before sending.
pub delivery_status: Option<DeliveryStatus>,
}

impl DbConnection {
pub fn fetch_conversation_list(&self) -> Result<Vec<ConversationListItem>, StorageError> {
let query = conversation_list
.select(conversation_list::all_columns())
.into_boxed();
Ok(self.raw_query(|conn| query.load::<ConversationListItem>(conn))?)
}
}

#[cfg(test)]
pub(crate) mod tests {
use crate::storage::group::tests::{generate_group, generate_group_with_created_at};
use crate::storage::tests::with_connection;
use crate::Store;
use wasm_bindgen_test::wasm_bindgen_test;

#[wasm_bindgen_test(unsupported = tokio::test)]
async fn test_single_group_multiple_messages() {
with_connection(|conn| {
// Create a group
let group = generate_group(None);
group.store(conn).unwrap();

// Insert multiple messages into the group
for i in 1..5 {
let message =
crate::storage::encrypted_store::group_message::tests::generate_message(
None,
Some(&group.id),
Some(i * 1000), // Increment timestamp for each message
);
message.store(conn).unwrap();
}

// Fetch the conversation list
let conversation_list = conn.fetch_conversation_list().unwrap();
assert_eq!(conversation_list.len(), 1, "Should return one group");
assert_eq!(
conversation_list[0].id, group.id,
"Returned group ID should match the created group"
);
assert_eq!(
conversation_list[0].sent_at_ns.unwrap(),
4000,
"Last message should be the most recent one"
);
})
.await
}
#[wasm_bindgen_test(unsupported = tokio::test)]
async fn test_three_groups_specific_ordering() {
with_connection(|conn| {
// Create three groups
let group_a = generate_group_with_created_at(None,5000); // Created after last message
let group_b = generate_group_with_created_at(None,2000); // Created before last message
let group_c = generate_group_with_created_at(None,1000); // Created before last message with no messages

group_a.store(conn).unwrap();
group_b.store(conn).unwrap();
group_c.store(conn).unwrap();
// Add a message to group_b
let message = crate::storage::encrypted_store::group_message::tests::generate_message(
None,
Some(&group_b.id),
Some(3000), // Last message timestamp
);
message.store(conn).unwrap();

// Fetch the conversation list
let conversation_list = conn.fetch_conversation_list().unwrap();

assert_eq!(conversation_list.len(), 3, "Should return all three groups");
assert_eq!(
conversation_list[0].id, group_a.id,
"Group created after the last message should come first"
);
assert_eq!(
conversation_list[1].id, group_b.id,
"Group with the last message should come second"
);
assert_eq!(
conversation_list[2].id, group_c.id,
"Group created before the last message with no messages should come last"
);
})
.await
}
#[wasm_bindgen_test(unsupported = tokio::test)]
async fn test_group_with_newer_message_update() {
with_connection(|conn| {
// Create a group
let group = generate_group(None);
group.store(conn).unwrap();

// Add an initial message
let first_message =
crate::storage::encrypted_store::group_message::tests::generate_message(
None,
Some(&group.id),
Some(1000),
);
first_message.store(conn).unwrap();

// Fetch the conversation list and check last message
let mut conversation_list = conn.fetch_conversation_list().unwrap();
assert_eq!(conversation_list.len(), 1, "Should return one group");
assert_eq!(
conversation_list[0].sent_at_ns.unwrap(),
1000,
"Last message should match the first message"
);

// Add a newer message
let second_message =
crate::storage::encrypted_store::group_message::tests::generate_message(
None,
Some(&group.id),
Some(2000),
);
second_message.store(conn).unwrap();

// Fetch the conversation list again and validate the last message is updated
conversation_list = conn.fetch_conversation_list().unwrap();
assert_eq!(
conversation_list[0].sent_at_ns.unwrap(),
2000,
"Last message should now match the second (newest) message"
);
})
.await
}
}
9 changes: 8 additions & 1 deletion xmtp_mls/src/storage/encrypted_store/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,8 +558,15 @@ pub(crate) mod tests {

/// Generate a test group
pub fn generate_group(state: Option<GroupMembershipState>) -> StoredGroup {
// Default behavior: Use `now_ns()` as the creation time
generate_group_with_created_at(state, now_ns())
}

pub fn generate_group_with_created_at(
state: Option<GroupMembershipState>,
created_at_ns: i64,
) -> StoredGroup {
let id = rand_vec::<24>();
let created_at_ns = now_ns();
let membership_state = state.unwrap_or(GroupMembershipState::Allowed);
StoredGroup::new(
id,
Expand Down
2 changes: 1 addition & 1 deletion xmtp_mls/src/storage/encrypted_store/group_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ pub(crate) mod tests {
use wasm_bindgen_test::wasm_bindgen_test;
use xmtp_common::{assert_err, assert_ok, rand_time, rand_vec};

fn generate_message(
pub fn generate_message(
kind: Option<GroupMessageKind>,
group_id: Option<&[u8]>,
sent_at_ns: Option<i64>,
Expand Down
1 change: 1 addition & 0 deletions xmtp_mls/src/storage/encrypted_store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub mod schema;
mod sqlcipher_connection;
pub mod user_preferences;
pub mod wallet_addresses;
mod conversation_list;
#[cfg(target_arch = "wasm32")]
mod wasm;

Expand Down
22 changes: 22 additions & 0 deletions xmtp_mls/src/storage/encrypted_store/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,27 @@ diesel::table! {
}
}

diesel::table!{
conversation_list (id) {
id -> Binary,
created_at_ns -> BigInt,
membership_state -> Integer,
installations_last_checked -> BigInt,
added_by_inbox_id -> Text,
welcome_id -> Nullable<BigInt>,
dm_inbox_id -> Nullable<Text>,
rotated_at_ns -> BigInt,
conversation_type -> Integer,
message_id -> Nullable<Binary>,
decrypted_message_bytes -> Nullable<Binary>,
sent_at_ns -> Nullable<BigInt>,
message_kind -> Nullable<Integer>,
sender_installation_id -> Nullable<Binary>,
sender_inbox_id -> Nullable<Text>,
delivery_status -> Nullable<Integer>,
}
}

diesel::joinable!(group_intents -> groups (group_id));
diesel::joinable!(group_messages -> groups (group_id));

Expand All @@ -138,4 +159,5 @@ diesel::allow_tables_to_appear_in_same_query!(
refresh_state,
user_preferences,
wallet_addresses,
conversation_list
);

0 comments on commit 03ba962

Please sign in to comment.