diff --git a/bindings_ffi/src/mls.rs b/bindings_ffi/src/mls.rs index 174614936..4361685ae 100644 --- a/bindings_ffi/src/mls.rs +++ b/bindings_ffi/src/mls.rs @@ -448,6 +448,7 @@ pub struct FfiPermissionPolicySet { pub update_group_name_policy: FfiPermissionPolicy, pub update_group_description_policy: FfiPermissionPolicy, pub update_group_image_url_square_policy: FfiPermissionPolicy, + pub update_group_pinned_frame_url_policy: FfiPermissionPolicy, } impl From for FfiGroupPermissionsOptions { @@ -464,6 +465,7 @@ pub enum FfiMetadataField { GroupName, Description, ImageUrlSquare, + PinnedFrameUrl, } impl From<&FfiMetadataField> for MetadataField { @@ -472,6 +474,7 @@ impl From<&FfiMetadataField> for MetadataField { FfiMetadataField::GroupName => MetadataField::GroupName, FfiMetadataField::Description => MetadataField::Description, FfiMetadataField::ImageUrlSquare => MetadataField::GroupImageUrlSquare, + FfiMetadataField::PinnedFrameUrl => MetadataField::GroupPinnedFrameUrl, } } } @@ -639,6 +642,7 @@ pub struct FfiCreateGroupOptions { pub group_name: Option, pub group_image_url_square: Option, pub group_description: Option, + pub group_pinned_frame_url: Option, } impl FfiCreateGroupOptions { @@ -647,6 +651,7 @@ impl FfiCreateGroupOptions { name: self.group_name, image_url_square: self.group_image_url_square, description: self.group_description, + pinned_frame_url: self.group_pinned_frame_url, } } } @@ -897,6 +902,35 @@ impl FfiGroup { Ok(group_description) } + pub async fn update_group_pinned_frame_url( + &self, + pinned_frame_url: String, + ) -> Result<(), GenericError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .update_group_pinned_frame_url(&self.inner_client, pinned_frame_url) + .await?; + + Ok(()) + } + + pub fn group_pinned_frame_url(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let group_pinned_frame_url = group.group_pinned_frame_url()?; + + Ok(group_pinned_frame_url) + } + pub fn admin_list(&self) -> Result, GenericError> { let group = MlsGroup::new( self.inner_client.context().clone(), @@ -1241,6 +1275,9 @@ impl FfiGroupPermissions { update_group_image_url_square_policy: get_policy( MetadataField::GroupImageUrlSquare.as_str(), ), + update_group_pinned_frame_url_policy: get_policy( + MetadataField::GroupPinnedFrameUrl.as_str(), + ), }) } } @@ -1570,6 +1607,7 @@ mod tests { group_name: Some("Group Name".to_string()), group_image_url_square: Some("url".to_string()), group_description: Some("group description".to_string()), + group_pinned_frame_url: Some("pinned frame".to_string()), }, ) .await @@ -1580,6 +1618,7 @@ mod tests { assert_eq!(group.group_name().unwrap(), "Group Name"); assert_eq!(group.group_image_url_square().unwrap(), "url"); assert_eq!(group.group_description().unwrap(), "group description"); + assert_eq!(group.group_pinned_frame_url().unwrap(), "pinned frame"); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -2131,6 +2170,7 @@ mod tests { update_group_name_policy: FfiPermissionPolicy::Admin, update_group_description_policy: FfiPermissionPolicy::Admin, update_group_image_url_square_policy: FfiPermissionPolicy::Admin, + update_group_pinned_frame_url_policy: FfiPermissionPolicy::Admin, }; assert_eq!(alix_permission_policy_set, expected_permission_policy_set); @@ -2159,6 +2199,7 @@ mod tests { update_group_name_policy: FfiPermissionPolicy::Allow, update_group_description_policy: FfiPermissionPolicy::Allow, update_group_image_url_square_policy: FfiPermissionPolicy::Allow, + update_group_pinned_frame_url_policy: FfiPermissionPolicy::Allow, }; assert_eq!(alix_permission_policy_set, expected_permission_policy_set); } @@ -2191,6 +2232,7 @@ mod tests { update_group_name_policy: FfiPermissionPolicy::Admin, update_group_description_policy: FfiPermissionPolicy::Admin, update_group_image_url_square_policy: FfiPermissionPolicy::Admin, + update_group_pinned_frame_url_policy: FfiPermissionPolicy::Admin, }; assert_eq!(alix_group_permissions, expected_permission_policy_set); @@ -2217,6 +2259,7 @@ mod tests { update_group_name_policy: FfiPermissionPolicy::Admin, update_group_description_policy: FfiPermissionPolicy::Admin, update_group_image_url_square_policy: FfiPermissionPolicy::Allow, + update_group_pinned_frame_url_policy: FfiPermissionPolicy::Admin, }; assert_eq!(alix_group_permissions, new_expected_permission_policy_set); diff --git a/bindings_node/src/conversations.rs b/bindings_node/src/conversations.rs index 298a6e21e..0ba01618e 100644 --- a/bindings_node/src/conversations.rs +++ b/bindings_node/src/conversations.rs @@ -28,6 +28,7 @@ pub struct NapiCreateGroupOptions { pub group_name: Option, pub group_image_url_square: Option, pub group_description: Option, + pub group_pinned_frame_url: Option, } impl NapiCreateGroupOptions { @@ -36,6 +37,7 @@ impl NapiCreateGroupOptions { name: self.group_name, image_url_square: self.group_image_url_square, description: self.group_description, + pinned_frame_url: self.group_pinned_frame_url, } } } @@ -64,6 +66,7 @@ impl NapiConversations { group_name: None, group_image_url_square: None, group_description: None, + group_pinned_frame_url: None, }, }; diff --git a/bindings_node/src/groups.rs b/bindings_node/src/groups.rs index 8f9236561..2f3ae2354 100644 --- a/bindings_node/src/groups.rs +++ b/bindings_node/src/groups.rs @@ -531,6 +531,37 @@ impl NapiGroup { Ok(group_description) } + #[napi] + pub async fn update_group_pinned_frame_url(&self, pinned_frame_url: String) -> Result<()> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .update_group_pinned_frame_url(&self.inner_client, pinned_frame_url) + .await + .map_err(|e| Error::from_reason(format!("{}", e)))?; + + Ok(()) + } + + #[napi] + pub fn group_pinned_frame_url(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let group_pinned_frame_url = group + .group_pinned_frame_url() + .map_err(|e| Error::from_reason(format!("{}", e)))?; + + Ok(group_pinned_frame_url) + } + #[napi(ts_args_type = "callback: (err: null | Error, result: NapiMessage) => void")] pub fn stream(&self, callback: JsFunction) -> Result { let tsfn: ThreadsafeFunction = diff --git a/bindings_node/test/Conversations.test.ts b/bindings_node/test/Conversations.test.ts index 186591c25..ac43bd36b 100644 --- a/bindings_node/test/Conversations.test.ts +++ b/bindings_node/test/Conversations.test.ts @@ -148,6 +148,19 @@ describe('Conversations', () => { expect(groupWithDescription.groupName()).toBe('') expect(groupWithDescription.groupImageUrlSquare()).toBe('') expect(groupWithDescription.groupDescription()).toBe('foo') + + const groupWithPinnedFrameUrl = await client1 + .conversations() + .createGroup([user2.account.address], { + groupPinnedFrameUrl: 'https://frameurl.xyz', + }) + expect(groupWithPinnedFrameUrl).toBeDefined() + expect(groupWithPinnedFrameUrl.groupName()).toBe('') + expect(groupWithPinnedFrameUrl.groupImageUrlSquare()).toBe('') + expect(groupWithPinnedFrameUrl.groupDescription()).toBe('') + expect(groupWithPinnedFrameUrl.groupPinnedFrameUrl()).toBe( + 'https://frameurl.xyz' + ) }) it('should update group metadata', async () => { @@ -167,6 +180,9 @@ describe('Conversations', () => { await group.updateGroupDescription('bar') expect(group.groupDescription()).toBe('bar') + + await group.updateGroupPinnedFrameUrl('https://frameurl.xyz') + expect(group.groupPinnedFrameUrl()).toBe('https://frameurl.xyz') }) it('should stream new groups', async () => { diff --git a/xmtp_mls/src/configuration.rs b/xmtp_mls/src/configuration.rs index 31671db79..e1dcf1fd3 100644 --- a/xmtp_mls/src/configuration.rs +++ b/xmtp_mls/src/configuration.rs @@ -44,6 +44,7 @@ pub const GROUP_PERMISSIONS_EXTENSION_ID: u16 = 0xff02; pub const DEFAULT_GROUP_NAME: &str = ""; pub const DEFAULT_GROUP_DESCRIPTION: &str = ""; pub const DEFAULT_GROUP_IMAGE_URL_SQUARE: &str = ""; +pub const DEFAULT_GROUP_PINNED_FRAME_URL: &str = ""; // If a metadata field name starts with this character, // and it does not have a policy set, it is a super admin only field diff --git a/xmtp_mls/src/groups/group_mutable_metadata.rs b/xmtp_mls/src/groups/group_mutable_metadata.rs index fa706cb1b..6b1eaf2fe 100644 --- a/xmtp_mls/src/groups/group_mutable_metadata.rs +++ b/xmtp_mls/src/groups/group_mutable_metadata.rs @@ -13,7 +13,7 @@ use xmtp_proto::xmtp::mls::message_contents::{ use crate::configuration::{ DEFAULT_GROUP_DESCRIPTION, DEFAULT_GROUP_IMAGE_URL_SQUARE, DEFAULT_GROUP_NAME, - MUTABLE_METADATA_EXTENSION_ID, + DEFAULT_GROUP_PINNED_FRAME_URL, MUTABLE_METADATA_EXTENSION_ID, }; use super::GroupMetadataOptions; @@ -42,6 +42,7 @@ pub enum MetadataField { GroupName, Description, GroupImageUrlSquare, + GroupPinnedFrameUrl, } impl MetadataField { @@ -50,6 +51,7 @@ impl MetadataField { MetadataField::GroupName => "group_name", MetadataField::Description => "description", MetadataField::GroupImageUrlSquare => "group_image_url_square", + MetadataField::GroupPinnedFrameUrl => "group_pinned_frame_url", } } } @@ -97,6 +99,11 @@ impl GroupMutableMetadata { opts.image_url_square .unwrap_or_else(|| DEFAULT_GROUP_IMAGE_URL_SQUARE.to_string()), ); + attributes.insert( + MetadataField::GroupPinnedFrameUrl.to_string(), + opts.pinned_frame_url + .unwrap_or_else(|| DEFAULT_GROUP_PINNED_FRAME_URL.to_string()), + ); let admin_list = vec![]; let super_admin_list = vec![creator_inbox_id.clone()]; Self { @@ -112,6 +119,7 @@ impl GroupMutableMetadata { MetadataField::GroupName, MetadataField::Description, MetadataField::GroupImageUrlSquare, + MetadataField::GroupPinnedFrameUrl, ] } diff --git a/xmtp_mls/src/groups/intents.rs b/xmtp_mls/src/groups/intents.rs index 15f61c044..aea0aa1d5 100644 --- a/xmtp_mls/src/groups/intents.rs +++ b/xmtp_mls/src/groups/intents.rs @@ -181,6 +181,13 @@ impl UpdateMetadataIntentData { field_value: group_description, } } + + pub fn new_update_group_pinned_frame_url(pinned_frame_url: String) -> Self { + Self { + field_name: MetadataField::GroupPinnedFrameUrl.to_string(), + field_value: pinned_frame_url, + } + } } impl From for Vec { diff --git a/xmtp_mls/src/groups/mod.rs b/xmtp_mls/src/groups/mod.rs index b70aec2d2..8349e0616 100644 --- a/xmtp_mls/src/groups/mod.rs +++ b/xmtp_mls/src/groups/mod.rs @@ -209,6 +209,7 @@ pub struct GroupMetadataOptions { pub name: Option, pub image_url_square: Option, pub description: Option, + pub pinned_frame_url: Option, } impl Clone for MlsGroup { @@ -736,6 +737,40 @@ impl MlsGroup { } } + pub async fn update_group_pinned_frame_url( + &self, + client: &Client, + pinned_frame_url: String, + ) -> Result<(), GroupError> + where + ApiClient: XmtpApi, + { + let conn = self.context.store.conn()?; + let intent_data: Vec = + UpdateMetadataIntentData::new_update_group_pinned_frame_url(pinned_frame_url).into(); + let intent = conn.insert_group_intent(NewGroupIntent::new( + IntentKind::MetadataUpdate, + self.group_id.clone(), + intent_data, + ))?; + + self.sync_until_intent_resolved(conn, intent.id, client) + .await + } + + pub fn group_pinned_frame_url(&self) -> Result { + let mutable_metadata = self.mutable_metadata()?; + match mutable_metadata + .attributes + .get(&MetadataField::GroupPinnedFrameUrl.to_string()) + { + Some(pinned_frame_url) => Ok(pinned_frame_url.clone()), + None => Err(GroupError::GroupMutableMetadata( + GroupMutableMetadataError::MissingExtension, + )), + } + } + pub fn admin_list(&self) -> Result, GroupError> { let mutable_metadata = self.mutable_metadata()?; Ok(mutable_metadata.admin_list) @@ -1785,6 +1820,7 @@ mod tests { name: Some("Group Name".to_string()), image_url_square: Some("url".to_string()), description: Some("group description".to_string()), + pinned_frame_url: Some("pinned frame".to_string()), }, ) .unwrap(); @@ -1802,10 +1838,15 @@ mod tests { .attributes .get(&MetadataField::Description.to_string()) .unwrap(); + let amal_group_pinned_frame_url: &String = binding + .attributes + .get(&MetadataField::GroupPinnedFrameUrl.to_string()) + .unwrap(); assert_eq!(amal_group_name, "Group Name"); assert_eq!(amal_group_image_url, "url"); assert_eq!(amal_group_description, "group description"); + assert_eq!(amal_group_pinned_frame_url, "pinned frame"); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -1848,7 +1889,7 @@ mod tests { amal_group.sync(&amal).await.unwrap(); let group_mutable_metadata = amal_group.mutable_metadata().unwrap(); - assert!(group_mutable_metadata.attributes.len().eq(&3)); + assert!(group_mutable_metadata.attributes.len().eq(&4)); assert!(group_mutable_metadata .attributes .get(&MetadataField::GroupName.to_string()) @@ -1951,6 +1992,40 @@ mod tests { assert_eq!(amal_group_image_url, "a url"); } + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_update_group_pinned_frame_url() { + let amal = ClientBuilder::new_test_client(&generate_local_wallet()).await; + + // Create a group and verify it has the default group name + let policies = Some(PreconfiguredPolicies::AdminsOnly); + let amal_group: MlsGroup = amal + .create_group(policies, GroupMetadataOptions::default()) + .unwrap(); + amal_group.sync(&amal).await.unwrap(); + + let group_mutable_metadata = amal_group.mutable_metadata().unwrap(); + assert!(group_mutable_metadata + .attributes + .get(&MetadataField::GroupPinnedFrameUrl.to_string()) + .unwrap() + .eq("")); + + // Update group name + amal_group + .update_group_pinned_frame_url(&amal, "a frame url".to_string()) + .await + .unwrap(); + + // Verify amal group sees update + amal_group.sync(&amal).await.unwrap(); + let binding = amal_group.mutable_metadata().expect("msg"); + let amal_group_pinned_frame_url: &String = binding + .attributes + .get(&MetadataField::GroupPinnedFrameUrl.to_string()) + .unwrap(); + assert_eq!(amal_group_pinned_frame_url, "a frame url"); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_group_mutable_data_group_permissions() { let amal = ClientBuilder::new_test_client(&generate_local_wallet()).await;