Skip to content

Commit d19a425

Browse files
fix: extend dialogManagerId's with dictinct strings (#2696)
### 🎯 Goal Rendering two Channel components side by side would result in inability to open any type of a dialog (rendered by default SDK components) in the other window. This PR adds unique strings to existing dialog manager ID's to prevent UI clashes. Fixes: #2685 Fixes: #2682
1 parent 3ca37b7 commit d19a425

File tree

10 files changed

+44
-26
lines changed

10 files changed

+44
-26
lines changed

β€Žsrc/components/Dialog/DialogManager.tsβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { nanoid } from 'nanoid';
12
import { StateStore } from 'stream-chat';
23

34
export type GetOrCreateDialogParams = {
@@ -43,7 +44,7 @@ export class DialogManager {
4344
});
4445

4546
constructor({ id }: DialogManagerOptions = {}) {
46-
this.id = id ?? new Date().getTime().toString();
47+
this.id = id ?? nanoid();
4748
}
4849

4950
get openDialogCount() {

β€Žsrc/components/Dialog/DialogPortal.tsxβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const DialogPortalDestination = () => {
1818
'--str-chat__dialog-overlay-height': openedDialogCount > 0 ? '100%' : '0',
1919
} as React.CSSProperties
2020
}
21-
></div>
21+
/>
2222
);
2323
};
2424

β€Žsrc/components/Dialog/__tests__/DialogsManager.test.jsβ€Ž

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DialogManager } from '../DialogManager';
2+
import * as nanoid from 'nanoid';
23

34
const dialogId = 'dialogId';
45

@@ -10,11 +11,9 @@ describe('DialogManager', () => {
1011
});
1112

1213
it('initiates with default options', () => {
13-
const mockedId = '12345';
14-
const spy = jest.spyOn(Date.prototype, 'getTime').mockReturnValueOnce(mockedId);
14+
jest.spyOn(nanoid, 'nanoid').mockReturnValue('mockedId');
1515
const dialogManager = new DialogManager();
16-
expect(dialogManager.id).toBe(mockedId);
17-
spy.mockRestore();
16+
expect(dialogManager.id).toBe('mockedId');
1817
});
1918

2019
it('creates a new closed dialog', () => {

β€Žsrc/components/MessageInput/AttachmentSelector.tsxβ€Ž

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
import { nanoid } from 'nanoid';
2-
import React, {
3-
ElementRef,
4-
useCallback,
5-
useEffect,
6-
useMemo,
7-
useRef,
8-
useState,
9-
} from 'react';
1+
import React, { ElementRef, useCallback, useEffect, useRef, useState } from 'react';
102
import { UploadIcon as DefaultUploadIcon } from './icons';
113
import { CHANNEL_CONTAINER_ID } from '../Channel/constants';
124
import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
@@ -25,16 +17,17 @@ import {
2517
AttachmentSelectorContextProvider,
2618
useAttachmentSelectorContext,
2719
} from '../../context/AttachmentSelectorContext';
20+
import { useStableId } from '../UtilityComponents/useStableId';
2821
import type { DefaultStreamChatGenerics } from '../../types';
2922

3023
export const SimpleAttachmentSelector = () => {
3124
const {
3225
AttachmentSelectorInitiationButtonContents,
3326
FileUploadIcon = DefaultUploadIcon,
3427
} = useComponentContext();
35-
const inputRef = useRef<ElementRef<'input'>>(null);
28+
const inputRef = useRef<HTMLInputElement | null>(null);
3629
const [labelElement, setLabelElement] = useState<HTMLLabelElement | null>(null);
37-
const id = useMemo(() => nanoid(), []);
30+
const id = useStableId();
3831

3932
useEffect(() => {
4033
if (!labelElement) return;

β€Žsrc/components/MessageInput/MessageInput.tsxβ€Ž

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
} from '../../context/ComponentContext';
1313
import { MessageInputContextProvider } from '../../context/MessageInputContext';
1414
import { DialogManagerProvider } from '../../context';
15+
import { useRegisterDropHandlers } from './WithDragAndDropUpload';
16+
import { useStableId } from '../UtilityComponents/useStableId';
1517

1618
import type { Channel, Message, SendFileAPIResponse } from 'stream-chat';
1719

@@ -26,7 +28,6 @@ import type {
2628
} from '../../types/types';
2729
import type { URLEnrichmentConfig } from './hooks/useLinkPreviews';
2830
import type { CustomAudioRecordingConfig } from '../MediaRecorder';
29-
import { useRegisterDropHandlers } from './WithDragAndDropUpload';
3031

3132
export type EmojiSearchIndexResult = {
3233
id: string;
@@ -174,10 +175,12 @@ const UnMemoizedMessageInput = <
174175
const { Input: ContextInput, TriggerProvider = DefaultTriggerProvider } =
175176
useComponentContext<StreamChatGenerics, V>('MessageInput');
176177

178+
const id = useStableId();
179+
177180
const Input = PropInput || ContextInput || MessageInputFlat;
178181
const dialogManagerId = props.isThreadInput
179-
? 'message-input-dialog-manager-thread'
180-
: 'message-input-dialog-manager';
182+
? `message-input-dialog-manager-thread-${id}`
183+
: `message-input-dialog-manager-${id}`;
181184

182185
if (dragAndDropWindow)
183186
return (

β€Žsrc/components/MessageList/MessageList.tsxβ€Ž

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading';
3434
import { defaultPinPermissions, MESSAGE_ACTIONS } from '../Message/utils';
3535
import { TypingIndicator as DefaultTypingIndicator } from '../TypingIndicator';
3636
import { MessageListMainPanel as DefaultMessageListMainPanel } from './MessageListMainPanel';
37-
37+
import { useStableId } from '../UtilityComponents/useStableId';
3838
import { defaultRenderMessages, MessageRenderer } from './renderMessages';
3939

4040
import type { GroupStyle, ProcessMessagesParams } from './utils';
@@ -225,10 +225,13 @@ const MessageListWithContext = <
225225
// eslint-disable-next-line react-hooks/exhaustive-deps
226226
}, [highlightedMessageId]);
227227

228+
const id = useStableId();
229+
228230
const showEmptyStateIndicator = elements.length === 0 && !threadList;
229231
const dialogManagerId = threadList
230-
? 'message-list-dialog-manager-thread'
231-
: 'message-list-dialog-manager';
232+
? `message-list-dialog-manager-thread-${id}`
233+
: `message-list-dialog-manager-${id}`;
234+
232235
return (
233236
<MessageListContextProvider value={{ listElement, scrollToBottom }}>
234237
<MessageListMainPanel>

β€Žsrc/components/MessageList/VirtualizedMessageList.tsxβ€Ž

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import type {
7272
} from 'stream-chat';
7373
import type { DefaultStreamChatGenerics, UnknownType } from '../../types/types';
7474
import { DEFAULT_NEXT_CHANNEL_PAGE_SIZE } from '../../constants/limits';
75+
import { useStableId } from '../UtilityComponents/useStableId';
7576

7677
type PropsDrilledToMessage =
7778
| 'additionalMessageInputProps'
@@ -456,11 +457,13 @@ const VirtualizedMessageListWithContext = <
456457
};
457458
}, [highlightedMessageId, processedMessages]);
458459

460+
const id = useStableId();
461+
459462
if (!processedMessages) return null;
460463

461464
const dialogManagerId = threadList
462-
? 'virtualized-message-list-dialog-manager-thread'
463-
: 'virtualized-message-list-dialog-manager';
465+
? `virtualized-message-list-dialog-manager-thread-${id}`
466+
: `virtualized-message-list-dialog-manager-${id}`;
464467

465468
return (
466469
<VirtualizedMessageListContextProvider value={{ scrollToBottom }}>

β€Žsrc/components/MessageList/__tests__/VirtualizedMessageList.test.jsβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { act } from 'react';
22
import { cleanup, render } from '@testing-library/react';
3+
import * as nanoid from 'nanoid';
34

45
import '@testing-library/jest-dom';
56

@@ -75,6 +76,8 @@ describe('VirtualizedMessageList', () => {
7576

7677
it('should render the list without any message', async () => {
7778
const { channel, client } = await createChannel(true);
79+
jest.spyOn(nanoid, 'nanoid').mockReturnValue('mockedId');
80+
7881
let result;
7982
await act(() => {
8083
result = render(

β€Žsrc/components/MessageList/__tests__/__snapshots__/VirtualizedMessageList.test.js.snapβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ exports[`VirtualizedMessageList should render the list without any message 1`] =
5555
</div>
5656
<div
5757
class="str-chat__dialog-overlay"
58-
data-str-chat__portal-id="virtualized-message-list-dialog-manager"
58+
data-str-chat__portal-id="virtualized-message-list-dialog-manager-mockedId"
5959
data-testid="str-chat__dialog-overlay"
6060
style="--str-chat__dialog-overlay-height: 0;"
6161
/>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { nanoid } from 'nanoid';
2+
import { useMemo } from 'react';
3+
4+
/**
5+
* The ID is generated using the `nanoid` library and is memoized to ensure
6+
* that it remains the same across renders unless the key changes.
7+
*/
8+
export const useStableId = (key?: string) => {
9+
// eslint-disable-next-line react-hooks/exhaustive-deps
10+
const id = useMemo(() => nanoid(), [key]);
11+
12+
return id;
13+
};

0 commit comments

Comments
Β (0)