From 7ab016c056ffb4037ad9e72c93b10c72866b79a2 Mon Sep 17 00:00:00 2001 From: Piotr Suwala Date: Thu, 26 Oct 2023 11:51:31 +0200 Subject: [PATCH] feat(lib): add default ids to channels, create a new event and forward messages to the same channel --- lib/src/entities/chat.ts | 61 ++++++++-- lib/src/types.ts | 4 + lib/src/uuidv4.ts | 56 ++++++++++ lib/tests/channel.test.ts | 105 ++++++++++++++++++ .../new-chat-screen/NewChatScreen.tsx | 8 -- .../new-group-screen/NewGroupScreen.tsx | 12 -- .../screens/tabs/home/HomeScreen.tsx | 27 +---- 7 files changed, 223 insertions(+), 50 deletions(-) create mode 100644 lib/src/uuidv4.ts diff --git a/lib/src/entities/chat.ts b/lib/src/entities/chat.ts index bda5f2a..0f940dc 100644 --- a/lib/src/entities/chat.ts +++ b/lib/src/entities/chat.ts @@ -19,6 +19,7 @@ import { ThreadChannel } from "./thread-channel" import { MentionsUtils } from "../mentions-utils" import { getErrorProxiedEntity, ErrorLogger } from "../error-logging" import { cyrb53a } from "../hash" +import { uuidv4 } from "../uuidv4" type ChatConfig = { saveDebugLog: boolean @@ -570,10 +571,16 @@ export class Chat { channelId, channelData, }: { - channelId: string + channelId?: string channelData: PubNub.ChannelMetadata }) { - return this.createChannel(channelId, { name: channelId, ...channelData, type: "public" }) + const finalChannelId = channelId || uuidv4() + + return this.createChannel(finalChannelId, { + name: finalChannelId, + ...channelData, + type: "public", + }) } /** @@ -617,11 +624,11 @@ export class Chat { async forwardMessage(message: Message, channel: string) { if (!channel) throw "Channel ID is required" if (!message) throw "Message is required" - if (message.channelId === channel) throw "You cannot forward the message to the same channel" const meta = { ...(message.meta || {}), originalPublisher: message.userId, + originalChannelId: message.channelId, } this.publish({ message: message.content, channel, meta }) } @@ -710,10 +717,12 @@ export class Chat { async createDirectConversation({ user, + channelId, channelData, membershipData = {}, }: { user: User + channelId?: string channelData: PubNub.ChannelMetadata membershipData?: Omit< PubNub.SetMembershipsParameters, @@ -729,11 +738,15 @@ export class Chat { const sortedUsers = [this.user.id, user.id].sort() - const channelId = `direct.${cyrb53a(`${sortedUsers[0]}&${sortedUsers[1]}`)}` + const finalChannelId = channelId || `direct.${cyrb53a(`${sortedUsers[0]}&${sortedUsers[1]}`)}` const channel = - (await this.getChannel(channelId)) || - (await this.createChannel(channelId, { name: channelId, ...channelData, type: "direct" })) + (await this.getChannel(finalChannelId)) || + (await this.createChannel(finalChannelId, { + name: finalChannelId, + ...channelData, + type: "direct", + })) const { custom, ...rest } = membershipData const hostMembershipPromise = this.sdk.objects.setMemberships({ @@ -753,6 +766,16 @@ export class Chat { channel.invite(user), ]) + await this.emitEvent({ + channel: user.id, + type: "invite", + method: "publish", + payload: { + channelType: "direct", + channelId: channel.id, + }, + }) + return { channel, hostMembership: Membership.fromMembershipDTO( @@ -774,7 +797,7 @@ export class Chat { membershipData = {}, }: { users: User[] - channelId: string + channelId?: string channelData: PubNub.ChannelMetadata membershipData?: Omit< PubNub.SetMembershipsParameters, @@ -783,10 +806,16 @@ export class Chat { custom?: PubNub.ObjectCustom } }) { + const finalChannelId = channelId || uuidv4() + try { const channel = - (await this.getChannel(channelId)) || - (await this.createChannel(channelId, { name: channelId, ...channelData, type: "group" })) + (await this.getChannel(finalChannelId)) || + (await this.createChannel(finalChannelId, { + name: finalChannelId, + ...channelData, + type: "group", + })) const { custom, ...rest } = membershipData const hostMembershipPromise = this.sdk.objects.setMemberships({ ...rest, @@ -805,6 +834,20 @@ export class Chat { channel.inviteMultiple(users), ]) + await Promise.all( + users.map(async (u) => { + await this.emitEvent({ + channel: u.id, + method: "publish", + type: "invite", + payload: { + channelType: "group", + channelId: channel.id, + }, + }) + }) + ) + return { channel, hostMembership: Membership.fromMembershipDTO( diff --git a/lib/src/types.ts b/lib/src/types.ts index e9d17e9..f935676 100644 --- a/lib/src/types.ts +++ b/lib/src/types.ts @@ -46,6 +46,10 @@ export type EventContent = { messageTimetoken: string channel: string } + invite: { + channelType: "direct" | "group" + channelId: string + } custom: any } diff --git a/lib/src/uuidv4.ts b/lib/src/uuidv4.ts new file mode 100644 index 0000000..06278a5 --- /dev/null +++ b/lib/src/uuidv4.ts @@ -0,0 +1,56 @@ +const byteToHex: string[] = [] + +for (let i = 0; i < 256; i++) { + byteToHex[i] = (i + 0x100).toString(16).substr(1) +} + +const unparse = (buf: Array, offset?: number) => { + let i = offset || 0 + const bth = byteToHex + + return ( + bth[buf[i++]] + + bth[buf[i++]] + + bth[buf[i++]] + + bth[buf[i++]] + + "-" + + bth[buf[i++]] + + bth[buf[i++]] + + "-" + + bth[buf[i++]] + + bth[buf[i++]] + + "-" + + bth[buf[i++]] + + bth[buf[i++]] + + "-" + + bth[buf[i++]] + + bth[buf[i++]] + + bth[buf[i++]] + + bth[buf[i++]] + + bth[buf[i++]] + + bth[buf[i++]] + ) +} + +const min = 0 +const max = 256 +const RANDOM_LENGTH = 16 + +export const rng = () => { + const result = new Array(RANDOM_LENGTH) + + for (let j = 0; j < RANDOM_LENGTH; j++) { + result[j] = 0xff & (Math.random() * (max - min) + min) + } + + return result +} + +export const uuidv4 = () => { + const rnds: number[] = rng() + + rnds[6] = (rnds[6] & 0x0f) | 0x40 + rnds[8] = (rnds[8] & 0x3f) | 0x80 + + return unparse(rnds) +} diff --git a/lib/tests/channel.test.ts b/lib/tests/channel.test.ts index 8a3dbf3..e1fceab 100644 --- a/lib/tests/channel.test.ts +++ b/lib/tests/channel.test.ts @@ -133,9 +133,86 @@ describe("Channel test", () => { await channel.leave() }) + test("should create a direct, group and public chats with a predefined ID", async () => { + const someFakeDirectId = "someFakeDirectId" + const someFakeGroupId = "someFakeGroupId" + const someFakePublicId = "someFakePublicId" + + const existingChannels = await Promise.all( + [someFakeDirectId, someFakeGroupId, someFakePublicId].map((id) => chat.getChannel(id)) + ) + + for (const existingChannel of existingChannels) { + if (existingChannel) { + await existingChannel.delete({ soft: false }) + } + } + + const user = await createRandomUser() + + const newChannels = await Promise.all([ + chat.createDirectConversation({ + user, + channelId: someFakeDirectId, + channelData: {}, + }), + chat.createGroupConversation({ + users: [user], + channelId: someFakeGroupId, + channelData: {}, + }), + chat.createPublicConversation({ + channelId: someFakePublicId, + channelData: {}, + }), + ]) + + expect(newChannels[0].channel.id).toBe(someFakeDirectId) + expect(newChannels[1].channel.id).toBe(someFakeGroupId) + expect(newChannels[2].id).toBe(someFakePublicId) + + await newChannels[0].channel.delete({ soft: false }) + await newChannels[1].channel.delete({ soft: false }) + await newChannels[2].delete({ soft: false }) + }) + + test("should create a direct, group and public chats with default IDs", async () => { + const user = await createRandomUser() + + const newChannels = await Promise.all([ + chat.createDirectConversation({ + user, + channelData: {}, + }), + chat.createGroupConversation({ + users: [user], + channelData: {}, + }), + chat.createPublicConversation({ + channelData: {}, + }), + ]) + + expect(newChannels[0].channel.id.startsWith("direct.")).toBeTruthy() + expect(newChannels[1].channel.id).toBeDefined() + expect(newChannels[2].id).toBeDefined() + + await newChannels[0].channel.delete({ soft: false }) + await newChannels[1].channel.delete({ soft: false }) + await newChannels[2].delete({ soft: false }) + }) + test("should create direct conversation and send message", async () => { const user = await createRandomUser() expect(user).toBeDefined() + const inviteCallback = jest.fn() + + const removeInvitationListener = chat.listenForEvents({ + channel: user.id, + type: "invite", + method: "publish", + callback: inviteCallback, + }) const directConversation = await chat.createDirectConversation({ user, @@ -151,13 +228,31 @@ describe("Channel test", () => { (message: Message) => message.content.text === messageText ) expect(messageInHistory).toBeTruthy() + expect(inviteCallback).toHaveBeenCalledTimes(1) + expect(inviteCallback).toHaveBeenCalledWith( + expect.objectContaining({ + payload: { + channelType: "direct", + channelId: directConversation.channel.id, + }, + }) + ) await user.delete() + removeInvitationListener() }) test("should create group conversation", async () => { const user1 = await createRandomUser() const user2 = await createRandomUser() const user3 = await createRandomUser() + const inviteCallback = jest.fn() + + const removeInvitationListener = chat.listenForEvents({ + channel: user1.id, + type: "invite", + method: "publish", + callback: inviteCallback, + }) const channelId = "group_channel_1234" const channelData = { @@ -190,11 +285,21 @@ describe("Channel test", () => { expect(channel.description).toEqual("This is a test group channel.") expect(channel.custom.groupInfo).toEqual("Additional group information") expect(inviteesMemberships.length).toEqual(3) + expect(inviteCallback).toHaveBeenCalledTimes(1) + expect(inviteCallback).toHaveBeenCalledWith( + expect.objectContaining({ + payload: { + channelType: "group", + channelId: result.channel.id, + }, + }) + ) await user1.delete() await user2.delete() await user3.delete() await channel.delete() + removeInvitationListener() }) test("should create a thread", async () => { diff --git a/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx b/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx index b840e56..2938949 100644 --- a/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx +++ b/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx @@ -21,14 +21,6 @@ export function NewChatScreen({ navigation }: StackScreenProps { - await chat.emitEvent({ - channel: u.id, - method: "publish", - payload: { - action: "GROUP_CONVERSATION_STARTED", - channelId: channel.id, - }, - }) - }) - ) setCurrentChannel(channel) setLoading(false) } diff --git a/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx b/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx index 91ab66e..d9a1b19 100644 --- a/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx +++ b/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx @@ -40,33 +40,18 @@ export function HomeScreen({ navigation }: StackScreenProps { - if (evt.payload.action === "DIRECT_CONVERSATION_STARTED") { - const { memberships } = await chat.currentUser.getMemberships() - setMemberships(memberships) - } - }, - }) - - const removeGroupChatListener = chat.listenForEvents({ - channel: chat.currentUser.id, - type: "custom", - method: "publish", - callback: async (evt) => { - if (evt.payload.action === "GROUP_CONVERSATION_STARTED") { - const { memberships } = await chat.currentUser.getMemberships() - setMemberships(memberships) - } + callback: async () => { + const { memberships } = await chat.currentUser.getMemberships() + setMemberships(memberships) }, }) return () => { - removeDirectChatListener() - removeGroupChatListener() + removeInvitationListener() } }, [chat])