Skip to content

Commit 137c2e7

Browse files
fix: extend dialogManagerId's with dictinct strings (#2696)
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 0a4ffde commit 137c2e7

File tree

10 files changed

+44
-19
lines changed

10 files changed

+44
-19
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
@@ -19,7 +19,7 @@ export const DialogPortalDestination = () => {
1919
'--str-chat__dialog-overlay-height': openedDialogCount > 0 ? '100%' : '0',
2020
} as React.CSSProperties
2121
}
22-
></div>
22+
/>
2323
);
2424
};
2525

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: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { nanoid } from 'nanoid';
2-
import type { ElementRef } from 'react';
3-
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1+
import React, { useCallback, useEffect, useRef, useState } from 'react';
42
import { UploadIcon as DefaultUploadIcon } from './icons';
53
import { useAttachmentManagerState } from './hooks/useAttachmentManagerState';
64
import { CHANNEL_CONTAINER_ID } from '../Channel/constants';
@@ -20,15 +18,16 @@ import {
2018
AttachmentSelectorContextProvider,
2119
useAttachmentSelectorContext,
2220
} from '../../context/AttachmentSelectorContext';
21+
import { useStableId } from '../UtilityComponents/useStableId';
2322

2423
export const SimpleAttachmentSelector = () => {
2524
const {
2625
AttachmentSelectorInitiationButtonContents,
2726
FileUploadIcon = DefaultUploadIcon,
2827
} = useComponentContext();
29-
const inputRef = useRef<ElementRef<'input'>>(null);
28+
const inputRef = useRef<HTMLInputElement | null>(null);
3029
const [labelElement, setLabelElement] = useState<HTMLLabelElement | null>(null);
31-
const id = useMemo(() => nanoid(), []);
30+
const id = useStableId();
3231

3332
useEffect(() => {
3433
if (!labelElement) return;
@@ -189,7 +188,7 @@ export const AttachmentSelector = ({
189188
const closeModal = useCallback(() => setModalContentActionAction(undefined), []);
190189

191190
const [fileInput, setFileInput] = useState<HTMLInputElement | null>(null);
192-
const menuButtonRef = useRef<ElementRef<'button'>>(null);
191+
const menuButtonRef = useRef<HTMLButtonElement>(null);
193192

194193
const getDefaultPortalDestination = useCallback(
195194
() => document.getElementById(CHANNEL_CONTAINER_ID),

src/components/MessageInput/MessageInput.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { ComponentContextValue } from '../../context/ComponentContext';
1010
import { useComponentContext } from '../../context/ComponentContext';
1111
import { MessageInputContextProvider } from '../../context/MessageInputContext';
1212
import { DialogManagerProvider } from '../../context';
13+
import { useStableId } from '../UtilityComponents/useStableId';
1314

1415
import type { LocalMessage, Message, SendMessageOptions } from 'stream-chat';
1516

@@ -134,10 +135,12 @@ const UnMemoizedMessageInput = (props: MessageInputProps) => {
134135

135136
const { Input: ContextInput } = useComponentContext('MessageInput');
136137

138+
const id = useStableId();
139+
137140
const Input = PropInput || ContextInput || MessageInputFlat;
138141
const dialogManagerId = props.isThreadInput
139-
? 'message-input-dialog-manager-thread'
140-
: 'message-input-dialog-manager';
142+
? `message-input-dialog-manager-thread-${id}`
143+
: `message-input-dialog-manager-${id}`;
141144

142145
return (
143146
<DialogManagerProvider id={dialogManagerId}>

src/components/MessageList/MessageList.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { TypingIndicator as DefaultTypingIndicator } from '../TypingIndicator';
2929
import { MessageListMainPanel as DefaultMessageListMainPanel } from './MessageListMainPanel';
3030

3131
import { defaultRenderMessages } from './renderMessages';
32+
import { useStableId } from '../UtilityComponents/useStableId';
3233

3334
import type { LocalMessage } from 'stream-chat';
3435
import type { MessageRenderer } from './renderMessages';
@@ -215,10 +216,13 @@ const MessageListWithContext = (props: MessageListWithContextProps) => {
215216
// eslint-disable-next-line react-hooks/exhaustive-deps
216217
}, [highlightedMessageId]);
217218

219+
const id = useStableId();
220+
218221
const showEmptyStateIndicator = elements.length === 0 && !threadList;
219222
const dialogManagerId = threadList
220-
? 'message-list-dialog-manager-thread'
221-
: 'message-list-dialog-manager';
223+
? `message-list-dialog-manager-thread-${id}`
224+
: `message-list-dialog-manager-${id}`;
225+
222226
return (
223227
<MessageListContextProvider value={{ listElement, scrollToBottom }}>
224228
<MessageListMainPanel>

src/components/MessageList/VirtualizedMessageList.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import type {
6666
} from 'stream-chat';
6767
import type { UnknownType } from '../../types/types';
6868
import { DEFAULT_NEXT_CHANNEL_PAGE_SIZE } from '../../constants/limits';
69+
import { useStableId } from '../UtilityComponents/useStableId';
6970

7071
type PropsDrilledToMessage =
7172
| 'additionalMessageInputProps'
@@ -437,11 +438,13 @@ const VirtualizedMessageListWithContext = (
437438
};
438439
}, [highlightedMessageId, processedMessages]);
439440

441+
const id = useStableId();
442+
440443
if (!processedMessages) return null;
441444

442445
const dialogManagerId = threadList
443-
? 'virtualized-message-list-dialog-manager-thread'
444-
: 'virtualized-message-list-dialog-manager';
446+
? `virtualized-message-list-dialog-manager-thread-${id}`
447+
: `virtualized-message-list-dialog-manager-${id}`;
445448

446449
return (
447450
<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)