diff --git a/packages/react-chat/src/components/Modals/EditModal.tsx b/packages/react-chat/src/components/Modals/EditModal.tsx index f05d5a3..750b9b1 100644 --- a/packages/react-chat/src/components/Modals/EditModal.tsx +++ b/packages/react-chat/src/components/Modals/EditModal.tsx @@ -20,7 +20,7 @@ import { export const EditModalName = "editModal"; export const EditModal = () => { - const { activeChannel } = useMessengerContext(); + const { activeChannel, changeGroupChatName } = useMessengerContext(); const [groupName, setGroupName] = useState(""); const [image, setImage] = useState(""); @@ -34,7 +34,7 @@ export const EditModal = () => { const handleUpload = () => { activeChannel.icon = image; - activeChannel.name = groupName; + changeGroupChatName(groupName, activeChannel.id); setModal(false); }; diff --git a/packages/react-chat/src/contexts/messengerProvider.tsx b/packages/react-chat/src/contexts/messengerProvider.tsx index 2061580..e4e9949 100644 --- a/packages/react-chat/src/contexts/messengerProvider.tsx +++ b/packages/react-chat/src/contexts/messengerProvider.tsx @@ -26,6 +26,7 @@ const MessengerContext = createContext({ removeChannel: () => undefined, setActiveChannel: () => undefined, createGroupChat: () => undefined, + changeGroupChatName: () => undefined, }); export function useMessengerContext() { diff --git a/packages/react-chat/src/hooks/messenger/useGroupChats.ts b/packages/react-chat/src/hooks/messenger/useGroupChats.ts index d83e8fa..e4cf07a 100644 --- a/packages/react-chat/src/hooks/messenger/useGroupChats.ts +++ b/packages/react-chat/src/hooks/messenger/useGroupChats.ts @@ -34,7 +34,7 @@ export function useGroupChats( const members = chat.members.map(contactFromId); const channel: ChannelData = { id: chat.chatId, - name: chat.chatId, + name: chat.name ?? chat.chatId, type: "group", members: members, }; @@ -78,6 +78,15 @@ export function useGroupChats( [groupChat] ); + const changeGroupChatName = useCallback( + (name: string, chatId: string) => { + if (groupChat) { + groupChat.changeChatName(chatId, name); + } + }, + [groupChat] + ); + const removeChannel = useCallback( (channelId: string) => { if (groupChat) { @@ -87,5 +96,5 @@ export function useGroupChats( [channels, groupChat] ); - return { createGroupChat, removeChannel, groupChat }; + return { createGroupChat, removeChannel, groupChat, changeGroupChatName }; } diff --git a/packages/react-chat/src/hooks/messenger/useMessenger.ts b/packages/react-chat/src/hooks/messenger/useMessenger.ts index a4b8f13..279a6e6 100644 --- a/packages/react-chat/src/hooks/messenger/useMessenger.ts +++ b/packages/react-chat/src/hooks/messenger/useMessenger.ts @@ -43,6 +43,7 @@ export type MessengerType = { activeChannel: ChannelData; setActiveChannel: (channel: ChannelData) => void; createGroupChat: (members: string[]) => void; + changeGroupChatName: (name: string, chatId: string) => void; }; function useCreateMessenger(identity: Identity | undefined) { @@ -167,14 +168,15 @@ export function useMessenger( }); }, [contacts]); - const { groupChat, removeChannel, createGroupChat } = useGroupChats( - messenger, - identity, - setChannels, - setActiveChannel, - addChatMessage, - channels - ); + const { groupChat, removeChannel, createGroupChat, changeGroupChatName } = + useGroupChats( + messenger, + identity, + setChannels, + setActiveChannel, + addChatMessage, + channels + ); const { loadPrevDay, loadingMessages } = useLoadPrevDay( activeChannel.id, @@ -232,5 +234,6 @@ export function useMessenger( mentions, clearMentions, createGroupChat, + changeGroupChatName, }; } diff --git a/packages/status-communities/src/groupChats.ts b/packages/status-communities/src/groupChats.ts index 266705d..94cd3b0 100644 --- a/packages/status-communities/src/groupChats.ts +++ b/packages/status-communities/src/groupChats.ts @@ -11,6 +11,8 @@ import { ChatMessage, ContentType } from "."; export type GroupChat = { chatId: string; members: string[]; + admins?: string[]; + name?: string; }; export type GroupChatsType = { @@ -91,37 +93,52 @@ export class GroupChats { const membershipUpdate = MembershipUpdateMessage.decode( message?.payload ); - if (membershipUpdate.events.length > 0) { - if ( - membershipUpdate.events[0].event.type == - MembershipUpdateEvent_EventType.CHAT_CREATED - ) { - await this.addChat( - { - chatId: membershipUpdate.chatId, - members: membershipUpdate.events[0].event.members, - }, - useCallback - ); - } - if ( - membershipUpdate.events[0].event.type == - MembershipUpdateEvent_EventType.MEMBER_REMOVED - ) { - if ( - membershipUpdate.events[0].event.members[0] == - bufToHex(this.identity.publicKey) - ) { - await this.removeChat( - { - chatId: membershipUpdate.chatId, - members: membershipUpdate.events[0].event.members, - }, - useCallback - ); + await Promise.all( + membershipUpdate.events.map(async (event) => { + const bufSigner = event.signer; + const signer = bufSigner ? bufToHex(bufSigner) : ""; + const chatId = membershipUpdate.chatId; + if (signer) { + switch (event.event.type) { + case MembershipUpdateEvent_EventType.CHAT_CREATED: { + await this.addChat( + { + chatId: chatId, + members: event.event.members, + admins: [signer], + }, + useCallback + ); + break; + } + case MembershipUpdateEvent_EventType.MEMBER_REMOVED: { + if ( + event.event.members[0] == bufToHex(this.identity.publicKey) + ) { + await this.removeChat( + { + chatId: chatId, + members: event.event.members, + }, + useCallback + ); + } + break; + } + case MembershipUpdateEvent_EventType.NAME_CHANGED: { + const chat = this.chats[chatId]; + if (chat) { + if (chat.admins?.includes(signer)) { + chat.name = event.event.name; + this.callback(chat); + } + } + break; + } + } } - } - } + }) + ); } } catch { return; @@ -207,6 +224,28 @@ export class GroupChats { ); } + private async sendUpdateMessage( + payload: Uint8Array, + members: string[] + ): Promise { + const wakuMessages = await Promise.all( + members.map( + async (member) => + await WakuMessage.fromBytes(payload, getPartitionedTopic(member)) + ) + ); + wakuMessages.forEach((msg) => this.waku.relay.send(msg)); + } + + public async changeChatName(chatId: string, name: string): Promise { + const payload = MembershipUpdateMessage.create(chatId, this.identity); + const chat = this.chats[chatId]; + if (chat && payload) { + payload.addNameChangeEvent(name); + await this.sendUpdateMessage(payload.encode(), chat.members); + } + } + /** * Sends a create group chat membership update message with given members * @@ -217,13 +256,7 @@ export class GroupChats { this.identity, members ).encode(); - const wakuMessages = await Promise.all( - members.map( - async (member) => - await WakuMessage.fromBytes(payload, getPartitionedTopic(member)) - ) - ); - wakuMessages.forEach((msg) => this.waku.relay.send(msg)); + await this.sendUpdateMessage(payload, members); } /** @@ -235,16 +268,7 @@ export class GroupChats { const payload = MembershipUpdateMessage.create(chatId, this.identity); const chat = this.chats[chatId]; payload.addMemberRemovedEvent(bufToHex(this.identity.publicKey)); - const wakuMessages = await Promise.all( - chat.members.map( - async (member) => - await WakuMessage.fromBytes( - payload.encode(), - getPartitionedTopic(member) - ) - ) - ); - wakuMessages.forEach((msg) => this.waku.relay.send(msg)); + await this.sendUpdateMessage(payload.encode(), chat.members); } /** diff --git a/packages/status-communities/src/identity.ts b/packages/status-communities/src/identity.ts index 368d505..c8919dd 100644 --- a/packages/status-communities/src/identity.ts +++ b/packages/status-communities/src/identity.ts @@ -19,7 +19,6 @@ export class Identity { */ public sign(payload: Uint8Array): Uint8Array { const hash = keccak256(payload); - const { signature, recid } = secp256k1.ecdsaSign( hexToBuf(hash), this.privateKey diff --git a/packages/status-communities/src/wire/membership_update_message.ts b/packages/status-communities/src/wire/membership_update_message.ts index cfb0061..bc4bd54 100644 --- a/packages/status-communities/src/wire/membership_update_message.ts +++ b/packages/status-communities/src/wire/membership_update_message.ts @@ -1,4 +1,6 @@ +import { keccak256 } from "js-sha3"; import { Reader } from "protobufjs"; +import * as secp256k1 from "secp256k1"; import { v4 as uuidV4 } from "uuid"; import { Identity } from ".."; @@ -34,6 +36,35 @@ export class MembershipUpdateEvent { } } +export class MembershipSignedEvent { + public sig: Uint8Array; + public event: MembershipUpdateEvent; + private chatId: string; + + public constructor( + sig: Uint8Array, + event: MembershipUpdateEvent, + chatId: string + ) { + this.sig = sig; + this.event = event; + this.chatId = chatId; + } + + public get signer(): Uint8Array | undefined { + const encEvent = this.event.encode(); + const eventToSign = Buffer.concat([hexToBuf(this.chatId), encEvent]); + + if (!this.sig || !eventToSign) return; + + const signature = this.sig.slice(0, 64); + const recid = this.sig.slice(64)[0]; + const hash = keccak256(eventToSign); + + return secp256k1.ecdsaRecover(signature, recid, hexToBuf(hash)); + } +} + export class MembershipUpdateMessage { private clock: number = Date.now(); private identity: Identity = Identity.generate(); @@ -150,15 +181,13 @@ export class MembershipUpdateMessage { return new MembershipUpdateMessage(protoBuf); } - public get events(): { - sig: Uint8Array; - event: MembershipUpdateEvent; - }[] { + public get events(): MembershipSignedEvent[] { return this.proto.events.map((bufArray) => { - return { - sig: bufArray.slice(0, 65), - event: MembershipUpdateEvent.decode(bufArray.slice(65)), - }; + return new MembershipSignedEvent( + bufArray.slice(0, 65), + MembershipUpdateEvent.decode(bufArray.slice(65)), + this.chatId + ); }); }