From 63c18fe8204af339c34bf96baae80eb232cd7736 Mon Sep 17 00:00:00 2001 From: martincupela Date: Tue, 19 Sep 2023 11:25:29 +0200 Subject: [PATCH 1/2] chore(deps): bump stream-chat to 8.12.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 526ca6c31..998738c98 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ "rollup-plugin-url": "^3.0.1", "rollup-plugin-visualizer": "^4.2.0", "semantic-release": "^19.0.5", - "stream-chat": "^8.4.1", + "stream-chat": "^8.12.0", "style-loader": "^2.0.0", "ts-jest": "^26.5.1", "typescript": "^4.7.4", diff --git a/yarn.lock b/yarn.lock index 7b0681b82..927575c9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13424,10 +13424,10 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" -stream-chat@^8.4.1: - version "8.10.1" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.10.1.tgz#9575a2b3b139906b7ca3bd5cd26b32f2ba2413f1" - integrity sha512-6vhpr4pH0NuPaKPm6c8BcBIKZPu218c8kiT0oA1Io05k+nZ6bzAs0+6dx/4t5djjTI4RcXQdZ4bdfaSAEmjWcw== +stream-chat@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.12.0.tgz#5b661242f24577fe7b4fd56555bbc47d4f652c48" + integrity sha512-JpH3QICvQ17m4zliOTB1ADuhEdkJSvfDUFJfj4F5ykkM7qvhz9FP/3STnx3W6ltC/ed9xhSYbVmBnA5HuWlEUw== dependencies: "@babel/runtime" "^7.16.3" "@types/jsonwebtoken" "~9.0.0" From eba7bbef33fe1d85ce6871ba43010b5b0489d032 Mon Sep 17 00:00:00 2001 From: MartinCupela <32706194+MartinCupela@users.noreply.github.com> Date: Tue, 19 Sep 2023 22:24:38 +0200 Subject: [PATCH 2/2] fix: keep channels initially without id registered for WS events (#2095) --- src/components/Channel/Channel.tsx | 22 +++- .../hooks/useChannelVisibleListener.ts | 8 +- .../useNotificationAddedToChannelListener.ts | 15 ++- .../useNotificationMessageNewListener.ts | 8 +- src/components/ChannelList/utils.ts | 37 +----- .../__tests__/ChatAutocomplete.test.js | 6 +- .../__tests__/LinkPreviewList.test.js | 22 ++-- .../__tests__/MessageInput.test.js | 10 +- src/index.ts | 1 + src/mock-builders/generator/channel.js | 4 +- src/utils/__tests__/getChannel.test.js | 111 ++++++++++++++++++ src/utils/getChannel.ts | 76 ++++++++++++ 12 files changed, 257 insertions(+), 63 deletions(-) create mode 100644 src/utils/__tests__/getChannel.test.js create mode 100644 src/utils/getChannel.ts diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index a220c2147..fe6d5ede8 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -12,6 +12,7 @@ import debounce from 'lodash.debounce'; import throttle from 'lodash.throttle'; import { ChannelAPIResponse, + ChannelMemberResponse, ChannelState, Event, logChatPromiseExecution, @@ -68,6 +69,7 @@ import { import { hasMoreMessagesProbably, hasNotMoreMessages } from '../MessageList/utils'; import defaultEmojiData from '../../stream-emoji.json'; import { makeAddNotifications } from './utils'; +import { getChannel } from '../../utils/getChannel'; import type { Data as EmojiMartData } from 'emoji-mart'; @@ -478,7 +480,25 @@ const ChannelInner = < (async () => { if (!channel.initialized) { try { - await channel.watch(); + // if active channel has been set without id, we will create a temporary channel id from its member IDs + // to keep track of the /query request in progress. This is the same approach of generating temporary id + // that the JS client uses to keep track of channel in client.activeChannels + const members: string[] = []; + if (!channel.id && channel.data?.members) { + for (const member of channel.data.members) { + let userId: string | undefined; + if (typeof member === 'string') { + userId = member; + } else if (typeof member === 'object') { + const { user, user_id } = member as ChannelMemberResponse; + userId = user_id || user?.id; + } + if (userId) { + members.push(userId); + } + } + } + await getChannel({ channel, client, members }); const config = channel.getConfig(); setChannelConfig(config); } catch (e) { diff --git a/src/components/ChannelList/hooks/useChannelVisibleListener.ts b/src/components/ChannelList/hooks/useChannelVisibleListener.ts index a097efdcd..d7a07ef7a 100644 --- a/src/components/ChannelList/hooks/useChannelVisibleListener.ts +++ b/src/components/ChannelList/hooks/useChannelVisibleListener.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import uniqBy from 'lodash.uniqby'; -import { getChannel } from '../utils'; +import { getChannel } from '../../../utils/getChannel'; import { useChatContext } from '../../../context/ChatContext'; @@ -25,7 +25,11 @@ export const useChannelVisibleListener = < if (customHandler && typeof customHandler === 'function') { customHandler(setChannels, event); } else if (event.type && event.channel_type && event.channel_id) { - const channel = await getChannel(client, event.channel_type, event.channel_id); + const channel = await getChannel({ + client, + id: event.channel_id, + type: event.channel_type, + }); setChannels((channels) => uniqBy([channel, ...channels], 'cid')); } }; diff --git a/src/components/ChannelList/hooks/useNotificationAddedToChannelListener.ts b/src/components/ChannelList/hooks/useNotificationAddedToChannelListener.ts index 5f36dfbad..b2d075b65 100644 --- a/src/components/ChannelList/hooks/useNotificationAddedToChannelListener.ts +++ b/src/components/ChannelList/hooks/useNotificationAddedToChannelListener.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import uniqBy from 'lodash.uniqby'; -import { getChannel } from '../utils'; +import { getChannel } from '../../../utils/getChannel'; import { useChatContext } from '../../../context/ChatContext'; @@ -26,7 +26,18 @@ export const useNotificationAddedToChannelListener = < if (customHandler && typeof customHandler === 'function') { customHandler(setChannels, event); } else if (allowNewMessagesFromUnfilteredChannels && event.channel?.type) { - const channel = await getChannel(client, event.channel.type, event.channel.id); + const channel = await getChannel({ + client, + id: event.channel.id, + members: event.channel.members?.reduce((acc, { user, user_id }) => { + const userId = user_id || user?.id; + if (userId) { + acc.push(userId); + } + return acc; + }, []), + type: event.channel.type, + }); setChannels((channels) => uniqBy([channel, ...channels], 'cid')); } }; diff --git a/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts b/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts index b8c10e7ba..94a65720d 100644 --- a/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts +++ b/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import uniqBy from 'lodash.uniqby'; -import { getChannel } from '../utils'; +import { getChannel } from '../../../utils/getChannel'; import { useChatContext } from '../../../context/ChatContext'; @@ -26,7 +26,11 @@ export const useNotificationMessageNewListener = < if (customHandler && typeof customHandler === 'function') { customHandler(setChannels, event); } else if (allowNewMessagesFromUnfilteredChannels && event.channel?.type) { - const channel = await getChannel(client, event.channel.type, event.channel.id); + const channel = await getChannel({ + client, + id: event.channel.id, + type: event.channel.type, + }); setChannels((channels) => uniqBy([channel, ...channels], 'cid')); } }; diff --git a/src/components/ChannelList/utils.ts b/src/components/ChannelList/utils.ts index f87f9b90b..6b7d3a5bc 100644 --- a/src/components/ChannelList/utils.ts +++ b/src/components/ChannelList/utils.ts @@ -1,43 +1,8 @@ -import type { Channel, QueryChannelAPIResponse, StreamChat } from 'stream-chat'; +import type { Channel } from 'stream-chat'; import uniqBy from 'lodash.uniqby'; import type { DefaultStreamChatGenerics } from '../../types/types'; -/** - * prevent from duplicate invocation of channel.watch() - * when events 'notification.message_new' and 'notification.added_to_channel' arrive at the same time - */ -const WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL: Record< - string, - Promise | undefined -> = {}; - -/** - * Calls channel.watch() if it was not already recently called. Waits for watch promise to resolve even if it was invoked previously. - * @param client - * @param type - * @param id - */ -export const getChannel = async < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics ->( - client: StreamChat, - type: string, - id: string, -) => { - const channel = client.channel(type, id); - const queryPromise = WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[channel.cid]; - if (queryPromise) { - await queryPromise; - } else { - WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[channel.cid] = channel.watch(); - await WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[channel.cid]; - WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[channel.cid] = undefined; - } - - return channel; -}; - export const MAX_QUERY_CHANNELS_LIMIT = 30; type MoveChannelUpParams< diff --git a/src/components/ChatAutoComplete/__tests__/ChatAutocomplete.test.js b/src/components/ChatAutoComplete/__tests__/ChatAutocomplete.test.js index c2750a492..471ea182c 100644 --- a/src/components/ChatAutoComplete/__tests__/ChatAutocomplete.test.js +++ b/src/components/ChatAutoComplete/__tests__/ChatAutocomplete.test.js @@ -83,13 +83,13 @@ describe('ChatAutoComplete', () => { beforeEach(async () => { const messages = [generateMessage({ user })]; const members = [generateMember({ user }), generateMember({ user: mentionUser })]; - const mockedChannel = generateChannel({ + const mockedChannelData = generateChannel({ members, messages, }); chatClient = await getTestClientWithUser(user); - useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); - channel = chatClient.channel('messaging', mockedChannel.id); + useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannelData)]); + channel = chatClient.channel('messaging', mockedChannelData.channel.id); }); afterEach(cleanup); diff --git a/src/components/MessageInput/__tests__/LinkPreviewList.test.js b/src/components/MessageInput/__tests__/LinkPreviewList.test.js index 6d10f5274..1173460c0 100644 --- a/src/components/MessageInput/__tests__/LinkPreviewList.test.js +++ b/src/components/MessageInput/__tests__/LinkPreviewList.test.js @@ -55,7 +55,7 @@ const threadMessage = generateMessage({ type: 'reply', user: user1, }); -const mockedChannel = generateChannel({ +const mockedChannelData = generateChannel({ members: [generateMember({ user: user1 }), generateMember({ user: mentionUser })], messages: [mainListMessage], thread: [threadMessage], @@ -177,14 +177,14 @@ describe('Link preview', () => { beforeEach(async () => { chatClient = await getTestClientWithUser({ id: user1.id }); - useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); - channel = chatClient.channel('messaging', mockedChannel.id); + useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannelData)]); + channel = chatClient.channel('messaging', mockedChannelData.channel.id); }); afterEach(tearDown); it('does not request URL enrichment if disabled in channel config', async () => { - const channel = chatClient.channel('messaging', mockedChannel.id); + const channel = chatClient.channel('messaging', mockedChannelData.channel.id); const { channel: { config }, } = generateChannel({ config: { url_enrichment: false } }); @@ -660,7 +660,7 @@ describe('Link preview', () => { }); it('are sent as attachments to posted message with skip_enrich_url:true', async () => { - const channel = chatClient.channel('messaging', mockedChannel.id); + const channel = chatClient.channel('messaging', mockedChannelData.channel.id); const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); jest @@ -716,7 +716,7 @@ describe('Link preview', () => { }); it('are not sent as attachments to posted message with skip_enrich_url:true if dismissed', async () => { - const channel = chatClient.channel('messaging', mockedChannel.id); + const channel = chatClient.channel('messaging', mockedChannelData.channel.id); const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); jest @@ -763,7 +763,7 @@ describe('Link preview', () => { }); it('does not add failed link previews among attachments', async () => { - const channel = chatClient.channel('messaging', mockedChannel.id); + const channel = chatClient.channel('messaging', mockedChannelData.channel.id); const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); jest @@ -804,7 +804,7 @@ describe('Link preview', () => { }); it('does not add dismissed link previews among attachments', async () => { - const channel = chatClient.channel('messaging', mockedChannel.id); + const channel = chatClient.channel('messaging', mockedChannelData.channel.id); const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); jest .spyOn(chatClient, 'enrichURL') @@ -1001,7 +1001,7 @@ describe('Link preview', () => { }); it('submit new message with skip_url_enrich:false if no link previews managed to get loaded', async () => { - const channel = chatClient.channel('messaging', mockedChannel.id); + const channel = chatClient.channel('messaging', mockedChannelData.channel.id); const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); let resolveEnrichURLPromise; jest @@ -1030,7 +1030,7 @@ describe('Link preview', () => { }); it('submit updated message with skip_url_enrich:false if no link previews managed to get loaded', async () => { - const channel = chatClient.channel('messaging', mockedChannel.id); + const channel = chatClient.channel('messaging', mockedChannelData.channel.id); const scrapedAudioAttachment = generateScrapedAudioAttachment({ og_scrape_url: 'http://getstream.io/audio', }); @@ -1348,7 +1348,7 @@ describe('Link preview', () => { }); it('link preview state is cleared after message submission', async () => { - const channel = chatClient.channel('messaging', mockedChannel.id); + const channel = chatClient.channel('messaging', mockedChannelData.channel.id); const sendMessageSpy = jest.spyOn(channel, 'sendMessage').mockImplementation(); let resolveEnrichURLPromise; jest diff --git a/src/components/MessageInput/__tests__/MessageInput.test.js b/src/components/MessageInput/__tests__/MessageInput.test.js index 319d6e6b9..d98065277 100644 --- a/src/components/MessageInput/__tests__/MessageInput.test.js +++ b/src/components/MessageInput/__tests__/MessageInput.test.js @@ -52,7 +52,7 @@ const threadMessage = generateMessage({ type: 'reply', user: user1, }); -const mockedChannel = generateChannel({ +const mockedChannelData = generateChannel({ members: [generateMember({ user: user1 }), generateMember({ user: mentionUser })], messages: [mainListMessage], thread: [threadMessage], @@ -162,8 +162,8 @@ function axeNoViolations(container) { describe(`${componentName}`, () => { beforeEach(async () => { chatClient = await getTestClientWithUser({ id: user1.id }); - useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); - channel = chatClient.channel('messaging', mockedChannel.id); + useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannelData)]); + channel = chatClient.channel('messaging', mockedChannelData.channel.id); }); afterEach(tearDown); @@ -1125,8 +1125,8 @@ function axeNoViolations(container) { describe(`${componentName}`, () => { beforeEach(async () => { chatClient = await getTestClientWithUser({ id: user1.id }); - useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); - channel = chatClient.channel('messaging', mockedChannel.id); + useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannelData)]); + channel = chatClient.channel('messaging', mockedChannelData.channel.id); }); afterEach(tearDown); diff --git a/src/index.ts b/src/index.ts index b9a4d16ba..a58b52f03 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export * from './context'; export * from './i18n'; // todo: distribute utils into separate files export * from './utils'; +export { getChannel } from './utils/getChannel'; diff --git a/src/mock-builders/generator/channel.js b/src/mock-builders/generator/channel.js index df0bfef43..602d13f63 100644 --- a/src/mock-builders/generator/channel.js +++ b/src/mock-builders/generator/channel.js @@ -4,6 +4,8 @@ export const generateChannel = (options = { channel: {} }) => { const { channel: optionsChannel, config, ...optionsBesidesChannel } = options; const id = optionsChannel?.id ?? nanoid(); const type = optionsChannel?.type ?? 'messaging'; + const { id: _, type: __, ...restOptionsChannel } = optionsChannel ?? {}; + return { members: [], messages: [], @@ -56,7 +58,7 @@ export const generateChannel = (options = { channel: {} }) => { id, type, updated_at: '2020-04-28T11:20:48.578147Z', - ...optionsChannel, + ...restOptionsChannel, }, }; }; diff --git a/src/utils/__tests__/getChannel.test.js b/src/utils/__tests__/getChannel.test.js new file mode 100644 index 000000000..89f82f865 --- /dev/null +++ b/src/utils/__tests__/getChannel.test.js @@ -0,0 +1,111 @@ +import { getChannel } from '../getChannel'; +import { + generateChannel, + generateMember, + generateUser, + getOrCreateChannelApi, + getTestClientWithUser, + useMockedApis, +} from '../../mock-builders'; + +let client; +const channelData = generateChannel({ + channel: { + members: [generateMember({ user: generateUser() }), generateMember({ user: generateUser() })], + }, +}); +const memberIds = channelData.channel.members.map((m) => m.user_id); +describe('getChannel', () => { + beforeEach(async () => { + client = await getTestClientWithUser(generateUser()); + useMockedApis(client, [getOrCreateChannelApi(channelData)]); + }); + + afterEach(jest.clearAllMocks); + + it('throws an error if neither channel nor channel type are provided', () => + expect(getChannel({ client, id: 'id', members: ['members'] })).rejects.toThrow( + 'Channel or channel type have to be provided to query a channel.', + )); + + it('throws an error if neither channel with id nor channel members array are provided', async () => { + const type = 'type'; + await expect(getChannel({ client, type })).rejects.toThrow( + 'Channel ID or channel members array have to be provided to query a channel.', + ); + }); + + it('throws an error if channel without with id and no channel members array are provided', async () => { + const channel = client.channel('type', undefined); + await expect(getChannel({ channel, client })).rejects.toThrow( + 'Channel ID or channel members array have to be provided to query a channel.', + ); + }); + + it('throws an error if channel without with id but with channel members array are provided', async () => { + const channel = client.channel('messaging', { members: memberIds }); + await expect(getChannel({ channel, client })).rejects.toThrow( + 'Channel ID or channel members array have to be provided to query a channel.', + ); + }); + + it('calls channel.watch for a given channel type and id if channel query not already in progress', async () => { + const channel = client.channel('messaging', channelData.channel.id); + jest.spyOn(channel, 'watch').mockResolvedValueOnce(); + await getChannel({ client, id: channel.id, type: channel.type }); + expect(channel.watch).toHaveBeenCalledTimes(1); + }); + + it('does not call channel.watch for a given channel type and id if channel query already in progress', () => { + const channel = client.channel('messaging', channelData.channel.id); + jest.spyOn(channel, 'watch').mockResolvedValue(); + getChannel({ client, id: channel.id, type: channel.type }); + getChannel({ client, id: channel.id, type: channel.type }); + expect(channel.watch).toHaveBeenCalledTimes(1); + }); + + it('calls channel.watch for a given channel type and members array if channel query not already in progress', async () => { + const channel = client.channel('messaging', { members: memberIds }); + jest.spyOn(channel, 'watch').mockResolvedValueOnce(); + await getChannel({ client, members: memberIds, type: channelData.channel.type }); + expect(channel.watch).toHaveBeenCalledTimes(1); + }); + + it('does not call channel.watch for a given channel type and members array if channel query already in progress', () => { + const channel = client.channel('messaging', { members: memberIds }); + jest.spyOn(channel, 'watch').mockResolvedValue(); + getChannel({ client, members: memberIds, type: channelData.channel.type }); + getChannel({ client, members: memberIds, type: channelData.channel.type }); + expect(channel.watch).toHaveBeenCalledTimes(1); + }); + + it('calls channel.watch for a given channel object with id and type if channel query not already in progress', async () => { + const channel = client.channel('messaging', 'id'); + jest.spyOn(channel, 'watch').mockResolvedValueOnce(); + await getChannel({ channel, client }); + expect(channel.watch).toHaveBeenCalledTimes(1); + }); + + it('does not call channel.watch for a given channel object with id and type if channel query already in progress', () => { + const channel = client.channel('messaging', 'id'); + jest.spyOn(channel, 'watch').mockResolvedValue(); + getChannel({ channel, client }); + getChannel({ channel, client }); + expect(channel.watch).toHaveBeenCalledTimes(1); + }); + + it('calls channel.watch for a given channel object with type and members array if channel query not already in progress', async () => { + const channel = client.channel('messaging', undefined); + jest.spyOn(channel, 'watch').mockResolvedValueOnce(); + await getChannel({ channel, client, members: memberIds }); + expect(channel.watch).toHaveBeenCalledTimes(1); + }); + + it('does not call channel.watch for a given channel object with type and members array if channel query already in progress', () => { + const channel = client.channel('messaging', undefined); + jest.spyOn(channel, 'watch').mockResolvedValue(); + getChannel({ channel, client, members: memberIds }); + getChannel({ channel, client, members: memberIds }); + expect(channel.watch).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/utils/getChannel.ts b/src/utils/getChannel.ts new file mode 100644 index 000000000..0a388f6bb --- /dev/null +++ b/src/utils/getChannel.ts @@ -0,0 +1,76 @@ +import type { Channel, QueryChannelAPIResponse, StreamChat } from 'stream-chat'; +import type { DefaultStreamChatGenerics } from '../types/types'; + +/** + * prevent from duplicate invocation of channel.watch() + * when events 'notification.message_new' and 'notification.added_to_channel' arrive at the same time + */ +const WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL: Record< + string, + Promise | undefined +> = {}; + +type GetChannelParams< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +> = { + client: StreamChat; + channel?: Channel; + id?: string; + members?: string[]; + type?: string; +}; +/** + * Calls channel.watch() if it was not already recently called. Waits for watch promise to resolve even if it was invoked previously. + * @param client + * @param members + * @param type + * @param id + * @param channel + */ +export const getChannel = async < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +>({ + channel, + client, + id, + members, + type, +}: GetChannelParams) => { + if (!channel && !type) { + throw new Error('Channel or channel type have to be provided to query a channel.'); + } + + // unfortunately typescript is not able to infer that if (!channel && !type) === false, then channel or type has to be truthy + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const theChannel = channel || client.channel(type!, id, { members }); + + // need to keep as with call to channel.watch the id can be changed from undefined to an actual ID generated server-side + const originalCid = theChannel?.id + ? theChannel.cid + : members && members.length + ? generateChannelTempCid(theChannel.type, members) + : undefined; + + if (!originalCid) { + throw new Error('Channel ID or channel members array have to be provided to query a channel.'); + } + + const queryPromise = WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid]; + + if (queryPromise) { + await queryPromise; + } else { + WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid] = theChannel.watch(); + await WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid]; + delete WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid]; + } + + return theChannel; +}; + +// Channels created without ID need to be referenced by an identifier until the back-end generates the final ID. +const generateChannelTempCid = (channelType: string, members?: string[]) => { + if (!members) return; + const membersStr = [...members].sort().join(','); + return `${channelType}:!members-${membersStr}`; +};