diff --git a/src/components/Dialog/DialogManager.ts b/src/components/Dialog/DialogManager.ts index 5ec28036b2..bc4ff06ce8 100644 --- a/src/components/Dialog/DialogManager.ts +++ b/src/components/Dialog/DialogManager.ts @@ -1,3 +1,4 @@ +import { nanoid } from 'nanoid'; import { StateStore } from 'stream-chat'; export type GetOrCreateDialogParams = { @@ -43,7 +44,7 @@ export class DialogManager { }); constructor({ id }: DialogManagerOptions = {}) { - this.id = id ?? new Date().getTime().toString(); + this.id = id ?? nanoid(); } get openDialogCount() { diff --git a/src/components/Dialog/DialogPortal.tsx b/src/components/Dialog/DialogPortal.tsx index aa814abacc..bc9d55aea1 100644 --- a/src/components/Dialog/DialogPortal.tsx +++ b/src/components/Dialog/DialogPortal.tsx @@ -18,7 +18,7 @@ export const DialogPortalDestination = () => { '--str-chat__dialog-overlay-height': openedDialogCount > 0 ? '100%' : '0', } as React.CSSProperties } - > + /> ); }; diff --git a/src/components/Dialog/__tests__/DialogsManager.test.js b/src/components/Dialog/__tests__/DialogsManager.test.js index 98c6580455..64099c1562 100644 --- a/src/components/Dialog/__tests__/DialogsManager.test.js +++ b/src/components/Dialog/__tests__/DialogsManager.test.js @@ -1,4 +1,5 @@ import { DialogManager } from '../DialogManager'; +import * as nanoid from 'nanoid'; const dialogId = 'dialogId'; @@ -10,11 +11,9 @@ describe('DialogManager', () => { }); it('initiates with default options', () => { - const mockedId = '12345'; - const spy = jest.spyOn(Date.prototype, 'getTime').mockReturnValueOnce(mockedId); + jest.spyOn(nanoid, 'nanoid').mockReturnValue('mockedId'); const dialogManager = new DialogManager(); - expect(dialogManager.id).toBe(mockedId); - spy.mockRestore(); + expect(dialogManager.id).toBe('mockedId'); }); it('creates a new closed dialog', () => { diff --git a/src/components/MessageInput/AttachmentSelector.tsx b/src/components/MessageInput/AttachmentSelector.tsx index 9446aabd8b..5dca80ff70 100644 --- a/src/components/MessageInput/AttachmentSelector.tsx +++ b/src/components/MessageInput/AttachmentSelector.tsx @@ -1,12 +1,4 @@ -import { nanoid } from 'nanoid'; -import React, { - ElementRef, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { ElementRef, useCallback, useEffect, useRef, useState } from 'react'; import { UploadIcon as DefaultUploadIcon } from './icons'; import { CHANNEL_CONTAINER_ID } from '../Channel/constants'; import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog'; @@ -25,6 +17,7 @@ import { AttachmentSelectorContextProvider, useAttachmentSelectorContext, } from '../../context/AttachmentSelectorContext'; +import { useStableId } from '../UtilityComponents/useStableId'; import type { DefaultStreamChatGenerics } from '../../types'; export const SimpleAttachmentSelector = () => { @@ -32,9 +25,9 @@ export const SimpleAttachmentSelector = () => { AttachmentSelectorInitiationButtonContents, FileUploadIcon = DefaultUploadIcon, } = useComponentContext(); - const inputRef = useRef>(null); + const inputRef = useRef(null); const [labelElement, setLabelElement] = useState(null); - const id = useMemo(() => nanoid(), []); + const id = useStableId(); useEffect(() => { if (!labelElement) return; diff --git a/src/components/MessageInput/MessageInput.tsx b/src/components/MessageInput/MessageInput.tsx index b2899ed70c..42aad1be96 100644 --- a/src/components/MessageInput/MessageInput.tsx +++ b/src/components/MessageInput/MessageInput.tsx @@ -12,6 +12,8 @@ import { } from '../../context/ComponentContext'; import { MessageInputContextProvider } from '../../context/MessageInputContext'; import { DialogManagerProvider } from '../../context'; +import { useRegisterDropHandlers } from './WithDragAndDropUpload'; +import { useStableId } from '../UtilityComponents/useStableId'; import type { Channel, Message, SendFileAPIResponse } from 'stream-chat'; @@ -26,7 +28,6 @@ import type { } from '../../types/types'; import type { URLEnrichmentConfig } from './hooks/useLinkPreviews'; import type { CustomAudioRecordingConfig } from '../MediaRecorder'; -import { useRegisterDropHandlers } from './WithDragAndDropUpload'; export type EmojiSearchIndexResult = { id: string; @@ -174,10 +175,12 @@ const UnMemoizedMessageInput = < const { Input: ContextInput, TriggerProvider = DefaultTriggerProvider } = useComponentContext('MessageInput'); + const id = useStableId(); + const Input = PropInput || ContextInput || MessageInputFlat; const dialogManagerId = props.isThreadInput - ? 'message-input-dialog-manager-thread' - : 'message-input-dialog-manager'; + ? `message-input-dialog-manager-thread-${id}` + : `message-input-dialog-manager-${id}`; if (dragAndDropWindow) return ( diff --git a/src/components/MessageList/MessageList.tsx b/src/components/MessageList/MessageList.tsx index 405288d056..d4bcaf4b4d 100644 --- a/src/components/MessageList/MessageList.tsx +++ b/src/components/MessageList/MessageList.tsx @@ -34,7 +34,7 @@ import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading'; import { defaultPinPermissions, MESSAGE_ACTIONS } from '../Message/utils'; import { TypingIndicator as DefaultTypingIndicator } from '../TypingIndicator'; import { MessageListMainPanel as DefaultMessageListMainPanel } from './MessageListMainPanel'; - +import { useStableId } from '../UtilityComponents/useStableId'; import { defaultRenderMessages, MessageRenderer } from './renderMessages'; import type { GroupStyle, ProcessMessagesParams } from './utils'; @@ -225,10 +225,13 @@ const MessageListWithContext = < // eslint-disable-next-line react-hooks/exhaustive-deps }, [highlightedMessageId]); + const id = useStableId(); + const showEmptyStateIndicator = elements.length === 0 && !threadList; const dialogManagerId = threadList - ? 'message-list-dialog-manager-thread' - : 'message-list-dialog-manager'; + ? `message-list-dialog-manager-thread-${id}` + : `message-list-dialog-manager-${id}`; + return ( diff --git a/src/components/MessageList/VirtualizedMessageList.tsx b/src/components/MessageList/VirtualizedMessageList.tsx index eae09ee050..3a81a278de 100644 --- a/src/components/MessageList/VirtualizedMessageList.tsx +++ b/src/components/MessageList/VirtualizedMessageList.tsx @@ -72,6 +72,7 @@ import type { } from 'stream-chat'; import type { DefaultStreamChatGenerics, UnknownType } from '../../types/types'; import { DEFAULT_NEXT_CHANNEL_PAGE_SIZE } from '../../constants/limits'; +import { useStableId } from '../UtilityComponents/useStableId'; type PropsDrilledToMessage = | 'additionalMessageInputProps' @@ -456,11 +457,13 @@ const VirtualizedMessageListWithContext = < }; }, [highlightedMessageId, processedMessages]); + const id = useStableId(); + if (!processedMessages) return null; const dialogManagerId = threadList - ? 'virtualized-message-list-dialog-manager-thread' - : 'virtualized-message-list-dialog-manager'; + ? `virtualized-message-list-dialog-manager-thread-${id}` + : `virtualized-message-list-dialog-manager-${id}`; return ( diff --git a/src/components/MessageList/__tests__/VirtualizedMessageList.test.js b/src/components/MessageList/__tests__/VirtualizedMessageList.test.js index c5269e5916..56a2a59436 100644 --- a/src/components/MessageList/__tests__/VirtualizedMessageList.test.js +++ b/src/components/MessageList/__tests__/VirtualizedMessageList.test.js @@ -1,5 +1,6 @@ import React, { act } from 'react'; import { cleanup, render } from '@testing-library/react'; +import * as nanoid from 'nanoid'; import '@testing-library/jest-dom'; @@ -75,6 +76,8 @@ describe('VirtualizedMessageList', () => { it('should render the list without any message', async () => { const { channel, client } = await createChannel(true); + jest.spyOn(nanoid, 'nanoid').mockReturnValue('mockedId'); + let result; await act(() => { result = render( diff --git a/src/components/MessageList/__tests__/__snapshots__/VirtualizedMessageList.test.js.snap b/src/components/MessageList/__tests__/__snapshots__/VirtualizedMessageList.test.js.snap index 757a5c2937..e20bc91d00 100644 --- a/src/components/MessageList/__tests__/__snapshots__/VirtualizedMessageList.test.js.snap +++ b/src/components/MessageList/__tests__/__snapshots__/VirtualizedMessageList.test.js.snap @@ -55,7 +55,7 @@ exports[`VirtualizedMessageList should render the list without any message 1`] =
diff --git a/src/components/UtilityComponents/useStableId.ts b/src/components/UtilityComponents/useStableId.ts new file mode 100644 index 0000000000..cb459e5533 --- /dev/null +++ b/src/components/UtilityComponents/useStableId.ts @@ -0,0 +1,13 @@ +import { nanoid } from 'nanoid'; +import { useMemo } from 'react'; + +/** + * The ID is generated using the `nanoid` library and is memoized to ensure + * that it remains the same across renders unless the key changes. + */ +export const useStableId = (key?: string) => { + // eslint-disable-next-line react-hooks/exhaustive-deps + const id = useMemo(() => nanoid(), [key]); + + return id; +};