stream-chat-react 13.5.1 → 13.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/components/Channel/Channel.d.ts +1 -1
  2. package/dist/components/Channel/Channel.js +2 -0
  3. package/dist/components/Chat/Chat.d.ts +1 -1
  4. package/dist/components/Chat/Chat.js +3 -1
  5. package/dist/components/Chat/hooks/useChat.js +1 -1
  6. package/dist/components/Dialog/DialogManager.d.ts +3 -1
  7. package/dist/components/Dialog/DialogManager.js +3 -0
  8. package/dist/components/Dialog/DialogPortal.d.ts +1 -1
  9. package/dist/components/Dialog/DialogPortal.js +4 -2
  10. package/dist/components/Dialog/hooks/useDialog.d.ts +11 -3
  11. package/dist/components/Dialog/hooks/useDialog.js +10 -7
  12. package/dist/components/Gallery/Gallery.js +2 -2
  13. package/dist/components/Gallery/Image.js +11 -5
  14. package/dist/components/Message/utils.js +2 -2
  15. package/dist/components/MessageBounce/MessageBounceModal.js +3 -2
  16. package/dist/components/MessageInput/AttachmentSelector.js +2 -1
  17. package/dist/components/MessageInput/EditMessageForm.js +2 -2
  18. package/dist/components/Modal/GlobalModal.d.ts +4 -0
  19. package/dist/components/Modal/GlobalModal.js +57 -0
  20. package/dist/components/Modal/Modal.d.ts +3 -4
  21. package/dist/components/Modal/index.d.ts +1 -0
  22. package/dist/components/Modal/index.js +1 -0
  23. package/dist/components/Poll/PollActions/PollAction.js +8 -4
  24. package/dist/components/Poll/PollActions/PollActions.js +8 -6
  25. package/dist/components/Reactions/ReactionsListModal.d.ts +1 -1
  26. package/dist/components/Reactions/ReactionsListModal.js +3 -2
  27. package/dist/components/TextareaComposer/TextareaComposer.js +15 -24
  28. package/dist/context/ComponentContext.d.ts +3 -1
  29. package/dist/context/DialogManagerContext.d.ts +21 -4
  30. package/dist/context/DialogManagerContext.js +114 -5
  31. package/dist/css/v2/index.css +1 -1
  32. package/dist/css/v2/index.layout.css +1 -1
  33. package/dist/experimental/index.browser.cjs +94 -22
  34. package/dist/experimental/index.browser.cjs.map +4 -4
  35. package/dist/experimental/index.node.cjs +94 -22
  36. package/dist/experimental/index.node.cjs.map +4 -4
  37. package/dist/index.browser.cjs +1767 -1545
  38. package/dist/index.browser.cjs.map +4 -4
  39. package/dist/index.node.cjs +1774 -1545
  40. package/dist/index.node.cjs.map +4 -4
  41. package/dist/scss/v2/Modal/Modal-theme.scss +21 -6
  42. package/dist/scss/v2/Poll/Poll-layout.scss +20 -6
  43. package/dist/scss/v2/Poll/Poll-theme.scss +8 -8
  44. package/package.json +4 -4
@@ -5,7 +5,7 @@ import type { OnMentionAction } from './hooks/useMentionsHandlers';
5
5
  import type { LoadingErrorIndicatorProps } from '../Loading';
6
6
  import type { ComponentContextValue } from '../../context';
7
7
  import type { ChannelUnreadUiState, GiphyVersions, ImageAttachmentSizeHandler, VideoAttachmentSizeHandler } from '../../types/types';
8
- type ChannelPropsForwardedToComponentContext = Pick<ComponentContextValue, 'Attachment' | 'AttachmentPreviewList' | 'AttachmentSelector' | 'AttachmentSelectorInitiationButtonContents' | 'AudioRecorder' | 'AutocompleteSuggestionItem' | 'AutocompleteSuggestionList' | 'Avatar' | 'BaseImage' | 'CooldownTimer' | 'CustomMessageActionsList' | 'DateSeparator' | 'EditMessageInput' | 'EmojiPicker' | 'emojiSearchIndex' | 'EmptyStateIndicator' | 'FileUploadIcon' | 'GiphyPreviewMessage' | 'HeaderComponent' | 'Input' | 'LinkPreviewList' | 'LoadingIndicator' | 'ShareLocationDialog' | 'Message' | 'MessageActions' | 'MessageBouncePrompt' | 'MessageBlocked' | 'MessageDeleted' | 'MessageIsThreadReplyInChannelButtonIndicator' | 'MessageListNotifications' | 'MessageListMainPanel' | 'MessageNotification' | 'MessageOptions' | 'MessageRepliesCountButton' | 'MessageStatus' | 'MessageSystem' | 'MessageTimestamp' | 'ModalGallery' | 'PinIndicator' | 'PollActions' | 'PollContent' | 'PollCreationDialog' | 'PollHeader' | 'PollOptionSelector' | 'QuotedMessage' | 'QuotedMessagePreview' | 'QuotedPoll' | 'reactionOptions' | 'ReactionSelector' | 'ReactionsList' | 'ReactionsListModal' | 'ReminderNotification' | 'SendButton' | 'SendToChannelCheckbox' | 'StartRecordingAudioButton' | 'TextareaComposer' | 'ThreadHead' | 'ThreadHeader' | 'ThreadStart' | 'Timestamp' | 'TypingIndicator' | 'UnreadMessagesNotification' | 'UnreadMessagesSeparator' | 'VirtualMessage' | 'StopAIGenerationButton' | 'StreamedMessageText'>;
8
+ type ChannelPropsForwardedToComponentContext = Pick<ComponentContextValue, 'Attachment' | 'AttachmentPreviewList' | 'AttachmentSelector' | 'AttachmentSelectorInitiationButtonContents' | 'AudioRecorder' | 'AutocompleteSuggestionItem' | 'AutocompleteSuggestionList' | 'Avatar' | 'BaseImage' | 'CooldownTimer' | 'CustomMessageActionsList' | 'DateSeparator' | 'EditMessageInput' | 'EmojiPicker' | 'emojiSearchIndex' | 'EmptyStateIndicator' | 'FileUploadIcon' | 'GiphyPreviewMessage' | 'HeaderComponent' | 'Input' | 'LinkPreviewList' | 'LoadingIndicator' | 'ShareLocationDialog' | 'Message' | 'MessageActions' | 'MessageBouncePrompt' | 'MessageBlocked' | 'MessageDeleted' | 'MessageIsThreadReplyInChannelButtonIndicator' | 'MessageListNotifications' | 'MessageListMainPanel' | 'MessageNotification' | 'MessageOptions' | 'MessageRepliesCountButton' | 'MessageStatus' | 'MessageSystem' | 'MessageTimestamp' | 'Modal' | 'ModalGallery' | 'PinIndicator' | 'PollActions' | 'PollContent' | 'PollCreationDialog' | 'PollHeader' | 'PollOptionSelector' | 'QuotedMessage' | 'QuotedMessagePreview' | 'QuotedPoll' | 'reactionOptions' | 'ReactionSelector' | 'ReactionsList' | 'ReactionsListModal' | 'ReminderNotification' | 'SendButton' | 'SendToChannelCheckbox' | 'StartRecordingAudioButton' | 'TextareaComposer' | 'ThreadHead' | 'ThreadHeader' | 'ThreadStart' | 'Timestamp' | 'TypingIndicator' | 'UnreadMessagesNotification' | 'UnreadMessagesSeparator' | 'VirtualMessage' | 'StopAIGenerationButton' | 'StreamedMessageText'>;
9
9
  export type ChannelProps = ChannelPropsForwardedToComponentContext & {
10
10
  /** Custom handler function that runs when the active channel has unread messages and the app is running on a separate browser tab */
11
11
  activeUnreadHandler?: (unread: number, documentTitle: string) => void;
@@ -747,6 +747,7 @@ const ChannelInner = (props) => {
747
747
  MessageStatus: props.MessageStatus,
748
748
  MessageSystem: props.MessageSystem,
749
749
  MessageTimestamp: props.MessageTimestamp,
750
+ Modal: props.Modal,
750
751
  ModalGallery: props.ModalGallery,
751
752
  PinIndicator: props.PinIndicator,
752
753
  PollActions: props.PollActions,
@@ -813,6 +814,7 @@ const ChannelInner = (props) => {
813
814
  props.MessageStatus,
814
815
  props.MessageSystem,
815
816
  props.MessageTimestamp,
817
+ props.Modal,
816
818
  props.ModalGallery,
817
819
  props.PinIndicator,
818
820
  props.PollActions,
@@ -3,7 +3,7 @@ import { SearchController } from 'stream-chat';
3
3
  import type { PropsWithChildren } from 'react';
4
4
  import type { StreamChat } from 'stream-chat';
5
5
  import type { CustomClasses } from '../../context/ChatContext';
6
- import type { MessageContextValue } from '../../context';
6
+ import { type MessageContextValue } from '../../context';
7
7
  import type { SupportedTranslations } from '../../i18n/types';
8
8
  import type { Streami18n } from '../../i18n/Streami18n';
9
9
  export type ChatProps = {
@@ -5,6 +5,7 @@ import { useCreateChatContext } from './hooks/useCreateChatContext';
5
5
  import { useChannelsQueryState } from './hooks/useChannelsQueryState';
6
6
  import { ChatProvider } from '../../context/ChatContext';
7
7
  import { TranslationProvider } from '../../context/TranslationContext';
8
+ import { ModalDialogManagerProvider } from '../../context';
8
9
  /**
9
10
  * Wrapper component for a StreamChat application. Chat needs to be placed around any other chat components
10
11
  * as it provides the ChatContext.
@@ -41,5 +42,6 @@ export const Chat = (props) => {
41
42
  if (!translators.t)
42
43
  return null;
43
44
  return (React.createElement(ChatProvider, { value: chatContextValue },
44
- React.createElement(TranslationProvider, { value: translators }, children)));
45
+ React.createElement(TranslationProvider, { value: translators },
46
+ React.createElement(ModalDialogManagerProvider, null, children))));
45
47
  };
@@ -24,7 +24,7 @@ export const useChat = ({ client, defaultLanguage = 'en', i18nInstance, initialN
24
24
  useEffect(() => {
25
25
  if (!client)
26
26
  return;
27
- const version = "13.5.1";
27
+ const version = "13.6.1";
28
28
  const userAgent = client.getUserAgent();
29
29
  if (!userAgent.includes('stream-chat-react')) {
30
30
  // result looks like: 'stream-chat-react-2.3.2-stream-chat-javascript-client-browser-2.2.2'
@@ -1,8 +1,9 @@
1
1
  /// <reference types="node" />
2
2
  import { StateStore } from 'stream-chat';
3
- export type GetOrCreateDialogParams = {
3
+ export type GetDialogParams = {
4
4
  id: DialogId;
5
5
  };
6
+ export type GetOrCreateDialogParams = GetDialogParams;
6
7
  type DialogId = string;
7
8
  export type Dialog = {
8
9
  close: () => void;
@@ -35,6 +36,7 @@ export declare class DialogManager {
35
36
  state: StateStore<DialogManagerState>;
36
37
  constructor({ id }?: DialogManagerOptions);
37
38
  get openDialogCount(): number;
39
+ get(id: DialogId): Dialog;
38
40
  getOrCreate({ id }: GetOrCreateDialogParams): Dialog;
39
41
  open(params: GetOrCreateDialogParams, closeRest?: boolean): void;
40
42
  close(id: DialogId): void;
@@ -24,6 +24,9 @@ export class DialogManager {
24
24
  return count;
25
25
  }, 0);
26
26
  }
27
+ get(id) {
28
+ return this.state.getLatestValue().dialogsById[id];
29
+ }
27
30
  getOrCreate({ id }) {
28
31
  let dialog = this.state.getLatestValue().dialogsById[id];
29
32
  if (!dialog) {
@@ -1,6 +1,6 @@
1
1
  import type { PropsWithChildren } from 'react';
2
2
  import React from 'react';
3
- export declare const DialogPortalDestination: () => React.JSX.Element;
3
+ export declare const DialogPortalDestination: () => React.JSX.Element | null;
4
4
  type DialogPortalEntryProps = {
5
5
  dialogId: string;
6
6
  };
@@ -5,13 +5,15 @@ import { useDialogManager } from '../../context';
5
5
  export const DialogPortalDestination = () => {
6
6
  const { dialogManager } = useDialogManager();
7
7
  const openedDialogCount = useOpenedDialogCount();
8
+ if (!openedDialogCount)
9
+ return null;
8
10
  return (React.createElement("div", { className: 'str-chat__dialog-overlay', "data-str-chat__portal-id": dialogManager.id, "data-testid": 'str-chat__dialog-overlay', onClick: () => dialogManager.closeAll(), style: {
9
11
  '--str-chat__dialog-overlay-height': openedDialogCount > 0 ? '100%' : '0',
10
12
  } }));
11
13
  };
12
14
  export const DialogPortalEntry = ({ children, dialogId, }) => {
13
- const { dialogManager } = useDialogManager();
14
- const dialogIsOpen = useDialogIsOpen(dialogId);
15
+ const { dialogManager } = useDialogManager({ dialogId });
16
+ const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager.id);
15
17
  const getPortalDestination = useCallback(() => document.querySelector(`div[data-str-chat__portal-id="${dialogManager.id}"]`), [dialogManager.id]);
16
18
  return (React.createElement(Portal, { getPortalDestination: getPortalDestination, isOpen: dialogIsOpen }, children));
17
19
  };
@@ -1,4 +1,12 @@
1
1
  import type { GetOrCreateDialogParams } from '../DialogManager';
2
- export declare const useDialog: ({ id }: GetOrCreateDialogParams) => import("../DialogManager").Dialog;
3
- export declare const useDialogIsOpen: (id: string) => boolean;
4
- export declare const useOpenedDialogCount: () => number;
2
+ export type UseDialogParams = GetOrCreateDialogParams & {
3
+ dialogManagerId?: string;
4
+ };
5
+ export declare const useDialog: ({ dialogManagerId, id }: UseDialogParams) => import("../DialogManager").Dialog;
6
+ export declare const modalDialogId: "modal-dialog";
7
+ export declare const useModalDialog: () => import("../DialogManager").Dialog;
8
+ export declare const useDialogIsOpen: (id: string, dialogManagerId?: string) => boolean;
9
+ export declare const useModalDialogIsOpen: () => boolean;
10
+ export declare const useOpenedDialogCount: ({ dialogManagerId, }?: {
11
+ dialogManagerId?: string;
12
+ }) => number;
@@ -1,8 +1,8 @@
1
1
  import { useCallback, useEffect } from 'react';
2
- import { useDialogManager } from '../../../context';
2
+ import { modalDialogManagerId, useDialogManager } from '../../../context';
3
3
  import { useStateStore } from '../../../store';
4
- export const useDialog = ({ id }) => {
5
- const { dialogManager } = useDialogManager();
4
+ export const useDialog = ({ dialogManagerId, id }) => {
5
+ const { dialogManager } = useDialogManager({ dialogManagerId });
6
6
  useEffect(() => () => {
7
7
  // Since this cleanup can run even if the component is still mounted
8
8
  // and dialog id is unchanged (e.g. in <StrictMode />), it's safer to
@@ -12,11 +12,14 @@ export const useDialog = ({ id }) => {
12
12
  }, [dialogManager, id]);
13
13
  return dialogManager.getOrCreate({ id });
14
14
  };
15
- export const useDialogIsOpen = (id) => {
16
- const { dialogManager } = useDialogManager();
15
+ export const modalDialogId = 'modal-dialog';
16
+ export const useModalDialog = () => useDialog({ dialogManagerId: modalDialogManagerId, id: modalDialogId });
17
+ export const useDialogIsOpen = (id, dialogManagerId) => {
18
+ const { dialogManager } = useDialogManager({ dialogManagerId });
17
19
  const dialogIsOpenSelector = useCallback(({ dialogsById }) => ({ isOpen: !!dialogsById[id]?.isOpen }), [id]);
18
20
  return useStateStore(dialogManager.state, dialogIsOpenSelector).isOpen;
19
21
  };
22
+ export const useModalDialogIsOpen = () => useDialogIsOpen(modalDialogId, modalDialogManagerId);
20
23
  const openedDialogCountSelector = (nextValue) => ({
21
24
  openedDialogCount: Object.values(nextValue.dialogsById).reduce((count, dialog) => {
22
25
  if (dialog.isOpen)
@@ -24,7 +27,7 @@ const openedDialogCountSelector = (nextValue) => ({
24
27
  return count;
25
28
  }, 0),
26
29
  });
27
- export const useOpenedDialogCount = () => {
28
- const { dialogManager } = useDialogManager();
30
+ export const useOpenedDialogCount = ({ dialogManagerId, } = {}) => {
31
+ const { dialogManager } = useDialogManager({ dialogManagerId });
29
32
  return useStateStore(dialogManager.state, openedDialogCountSelector).openedDialogCount;
30
33
  };
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
2
2
  import { sanitizeUrl } from '@braintree/sanitize-url';
3
3
  import clsx from 'clsx';
4
4
  import { BaseImage as DefaultBaseImage } from './BaseImage';
5
- import { Modal } from '../Modal';
5
+ import { Modal as DefaultModal } from '../Modal';
6
6
  import { ModalGallery as DefaultModalGallery } from './ModalGallery';
7
7
  import { useComponentContext } from '../../context/ComponentContext';
8
8
  import { useTranslationContext } from '../../context/TranslationContext';
@@ -10,7 +10,7 @@ const UnMemoizedGallery = (props) => {
10
10
  const { images, innerRefs } = props;
11
11
  const [index, setIndex] = useState(0);
12
12
  const [modalOpen, setModalOpen] = useState(false);
13
- const { BaseImage = DefaultBaseImage, ModalGallery = DefaultModalGallery } = useComponentContext('Gallery');
13
+ const { BaseImage = DefaultBaseImage, Modal = DefaultModal, ModalGallery = DefaultModalGallery, } = useComponentContext('Gallery');
14
14
  const { t } = useTranslationContext('Gallery');
15
15
  const imageFallbackTitle = t('User uploaded content');
16
16
  const countImagesDisplayedInPreview = 4;
@@ -1,7 +1,8 @@
1
+ import { useCallback } from 'react';
1
2
  import React, { useState } from 'react';
2
3
  import { sanitizeUrl } from '@braintree/sanitize-url';
3
4
  import { BaseImage as DefaultBaseImage } from './BaseImage';
4
- import { Modal } from '../Modal';
5
+ import { Modal as DefaultModal } from '../Modal';
5
6
  import { ModalGallery as DefaultModalGallery } from './ModalGallery';
6
7
  import { useComponentContext } from '../../context';
7
8
  /**
@@ -10,11 +11,16 @@ import { useComponentContext } from '../../context';
10
11
  export const ImageComponent = (props) => {
11
12
  const { dimensions = {}, fallback, image_url, innerRef, previewUrl, style, thumb_url, } = props;
12
13
  const [modalIsOpen, setModalIsOpen] = useState(false);
13
- const { BaseImage = DefaultBaseImage, ModalGallery = DefaultModalGallery } = useComponentContext('ImageComponent');
14
+ const { BaseImage = DefaultBaseImage, Modal = DefaultModal, ModalGallery = DefaultModalGallery, } = useComponentContext('ImageComponent');
14
15
  const imageSrc = sanitizeUrl(previewUrl || image_url || thumb_url);
15
- const toggleModal = () => setModalIsOpen((modalIsOpen) => !modalIsOpen);
16
+ const closeModal = useCallback(() => {
17
+ setModalIsOpen(false);
18
+ }, []);
19
+ const openModal = useCallback(() => {
20
+ setModalIsOpen(true);
21
+ }, []);
16
22
  return (React.createElement(React.Fragment, null,
17
- React.createElement(BaseImage, { alt: fallback, className: 'str-chat__message-attachment--img', "data-testid": 'image-test', onClick: toggleModal, src: imageSrc, style: style, tabIndex: 0, title: fallback, ...dimensions, ...(innerRef && { ref: innerRef }) }),
18
- React.createElement(Modal, { className: 'str-chat__image-modal', onClose: toggleModal, open: modalIsOpen },
23
+ React.createElement(BaseImage, { alt: fallback, className: 'str-chat__message-attachment--img', "data-testid": 'image-test', onClick: openModal, src: imageSrc, style: style, tabIndex: 0, title: fallback, ...dimensions, ...(innerRef && { ref: innerRef }) }),
24
+ React.createElement(Modal, { className: 'str-chat__image-modal', onClose: closeModal, open: modalIsOpen },
19
25
  React.createElement(ModalGallery, { images: [props], index: 0 }))));
20
26
  };
@@ -135,14 +135,14 @@ export const getMessageActions = (actions, { canDelete, canEdit, canFlag, canMar
135
135
  messageActionsAfterPermission.push(MESSAGE_ACTIONS.react);
136
136
  }
137
137
  if (channelConfig?.['user_message_reminders'] &&
138
- messageActions.indexOf(MESSAGE_ACTIONS.remindMe)) {
138
+ messageActions.indexOf(MESSAGE_ACTIONS.remindMe) > -1) {
139
139
  messageActionsAfterPermission.push(MESSAGE_ACTIONS.remindMe);
140
140
  }
141
141
  if (canReply && messageActions.indexOf(MESSAGE_ACTIONS.reply) > -1) {
142
142
  messageActionsAfterPermission.push(MESSAGE_ACTIONS.reply);
143
143
  }
144
144
  if (channelConfig?.['user_message_reminders'] &&
145
- messageActions.indexOf(MESSAGE_ACTIONS.saveForLater)) {
145
+ messageActions.indexOf(MESSAGE_ACTIONS.saveForLater) > -1) {
146
146
  messageActionsAfterPermission.push(MESSAGE_ACTIONS.saveForLater);
147
147
  }
148
148
  return messageActionsAfterPermission;
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
- import { Modal } from '../Modal';
3
- import { MessageBounceProvider } from '../../context';
2
+ import { Modal as DefaultModal } from '../Modal';
3
+ import { MessageBounceProvider, useComponentContext } from '../../context';
4
4
  export function MessageBounceModal({ MessageBouncePrompt, ...modalProps }) {
5
+ const { Modal = DefaultModal } = useComponentContext();
5
6
  return (React.createElement(Modal, { className: 'str-chat__message-bounce-modal', ...modalProps },
6
7
  React.createElement(MessageBounceProvider, null,
7
8
  React.createElement(MessageBouncePrompt, { onClose: modalProps.onClose }))));
@@ -4,7 +4,7 @@ import { useAttachmentManagerState } from './hooks/useAttachmentManagerState';
4
4
  import { CHANNEL_CONTAINER_ID } from '../Channel/constants';
5
5
  import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
6
6
  import { DialogMenuButton } from '../Dialog/DialogMenu';
7
- import { Modal } from '../Modal';
7
+ import { Modal as DefaultModal } from '../Modal';
8
8
  import { ShareLocationDialog as DefaultLocationDialog } from '../Location';
9
9
  import { PollCreationDialog as DefaultPollCreationDialog } from '../Poll';
10
10
  import { Portal } from '../Portal/Portal';
@@ -111,6 +111,7 @@ const useAttachmentSelectorActionsFiltered = (original) => {
111
111
  };
112
112
  export const AttachmentSelector = ({ attachmentSelectorActionSet = defaultAttachmentSelectorActionSet, getModalPortalDestination, }) => {
113
113
  const { t } = useTranslationContext();
114
+ const { Modal = DefaultModal } = useComponentContext();
114
115
  const { channelCapabilities } = useChannelStateContext();
115
116
  const messageComposer = useMessageComposer();
116
117
  const actions = useAttachmentSelectorActionsFiltered(attachmentSelectorActionSet);
@@ -1,7 +1,7 @@
1
1
  import React, { useCallback, useEffect } from 'react';
2
2
  import { MessageInput } from './MessageInput';
3
3
  import { MessageInputFlat } from './MessageInputFlat';
4
- import { Modal } from '../Modal';
4
+ import { Modal as DefaultModal } from '../Modal';
5
5
  import { useComponentContext, useMessageContext, useMessageInputContext, useTranslationContext, } from '../../context';
6
6
  import { useMessageComposer, useMessageComposerHasSendableData } from './hooks';
7
7
  const EditMessageFormSendButton = () => {
@@ -32,7 +32,7 @@ export const EditMessageForm = () => {
32
32
  React.createElement(EditMessageFormSendButton, null))));
33
33
  };
34
34
  export const EditMessageModal = ({ additionalMessageInputProps, }) => {
35
- const { EditMessageInput = EditMessageForm } = useComponentContext();
35
+ const { EditMessageInput = EditMessageForm, Modal = DefaultModal } = useComponentContext();
36
36
  const { clearEditingState } = useMessageContext();
37
37
  const messageComposer = useMessageComposer();
38
38
  const onEditModalClose = useCallback(() => {
@@ -0,0 +1,4 @@
1
+ import type { PropsWithChildren } from 'react';
2
+ import React from 'react';
3
+ import type { ModalProps } from './Modal';
4
+ export declare const GlobalModal: ({ children, className, onClose, onCloseAttempt, open, }: PropsWithChildren<ModalProps>) => React.JSX.Element | null;
@@ -0,0 +1,57 @@
1
+ import clsx from 'clsx';
2
+ import { useCallback } from 'react';
3
+ import React, { useEffect, useRef } from 'react';
4
+ import { FocusScope } from '@react-aria/focus';
5
+ import { CloseIconRound } from './icons';
6
+ import { useTranslationContext } from '../../context';
7
+ import { DialogPortalEntry, modalDialogId, useModalDialog, useModalDialogIsOpen, } from '../Dialog';
8
+ export const GlobalModal = ({ children, className, onClose, onCloseAttempt, open, }) => {
9
+ const { t } = useTranslationContext('Modal');
10
+ const dialog = useModalDialog();
11
+ const isOpen = useModalDialogIsOpen();
12
+ const innerRef = useRef(null);
13
+ const closeButtonRef = useRef(null);
14
+ const maybeClose = useCallback((source, event) => {
15
+ const allow = onCloseAttempt?.(source, event);
16
+ if (allow !== false) {
17
+ onClose?.(event);
18
+ dialog.close();
19
+ }
20
+ }, [dialog, onClose, onCloseAttempt]);
21
+ const handleClick = (event) => {
22
+ const target = event.target;
23
+ if (!innerRef.current || !closeButtonRef.current)
24
+ return;
25
+ if (innerRef.current?.contains(target))
26
+ return;
27
+ if (closeButtonRef.current.contains(target)) {
28
+ maybeClose('button', event);
29
+ }
30
+ else if (!innerRef.current.contains(target)) {
31
+ maybeClose('overlay', event);
32
+ }
33
+ };
34
+ useEffect(() => {
35
+ if (!isOpen)
36
+ return;
37
+ const handleKeyDown = (event) => {
38
+ if (event.key === 'Escape')
39
+ maybeClose('escape', event);
40
+ };
41
+ document.addEventListener('keydown', handleKeyDown);
42
+ return () => document.removeEventListener('keydown', handleKeyDown);
43
+ }, [isOpen, maybeClose]);
44
+ useEffect(() => {
45
+ if (open && !dialog.isOpen) {
46
+ dialog.open();
47
+ }
48
+ }, [dialog, open]);
49
+ if (!open || !isOpen)
50
+ return null;
51
+ return (React.createElement(DialogPortalEntry, { dialogId: modalDialogId },
52
+ React.createElement("div", { className: clsx('str-chat str-chat__modal str-chat-react__modal str-chat__modal--open', className), onClick: handleClick },
53
+ React.createElement(FocusScope, { autoFocus: true, contain: true },
54
+ React.createElement("button", { className: 'str-chat__modal__close-button', ref: closeButtonRef, title: t('Close'), type: 'button' },
55
+ React.createElement(CloseIconRound, null)),
56
+ React.createElement("div", { className: 'str-chat__modal__inner str-chat-react__modal__inner', ref: innerRef }, children)))));
57
+ };
@@ -1,6 +1,6 @@
1
1
  import { type PropsWithChildren } from 'react';
2
2
  import React from 'react';
3
- type CloseEvent = KeyboardEvent | React.KeyboardEvent | React.MouseEvent<HTMLButtonElement | HTMLDivElement>;
3
+ export type ModalCloseEvent = KeyboardEvent | React.KeyboardEvent | React.MouseEvent<HTMLButtonElement | HTMLDivElement>;
4
4
  export type ModalCloseSource = 'overlay' | 'button' | 'escape';
5
5
  export type ModalProps = {
6
6
  /** If true, modal is opened or visible. */
@@ -8,9 +8,8 @@ export type ModalProps = {
8
8
  /** Custom class to be applied to the modal root div */
9
9
  className?: string;
10
10
  /** Callback handler for closing of modal. */
11
- onClose?: (event: CloseEvent) => void;
11
+ onClose?: (event: ModalCloseEvent) => void;
12
12
  /** Optional handler to intercept closing logic. Return false to prevent onClose. */
13
- onCloseAttempt?: (source: ModalCloseSource, event: CloseEvent) => boolean;
13
+ onCloseAttempt?: (source: ModalCloseSource, event: ModalCloseEvent) => boolean;
14
14
  };
15
15
  export declare const Modal: ({ children, className, onClose, onCloseAttempt, open, }: PropsWithChildren<ModalProps>) => React.JSX.Element | null;
16
- export {};
@@ -1 +1,2 @@
1
+ export * from './GlobalModal';
1
2
  export * from './Modal';
@@ -1 +1,2 @@
1
+ export * from './GlobalModal';
1
2
  export * from './Modal';
@@ -1,5 +1,9 @@
1
1
  import React from 'react';
2
- import { Modal } from '../../Modal';
3
- export const PollAction = ({ buttonText, children, closeModal, modalClassName, modalIsOpen, openModal, }) => (React.createElement(React.Fragment, null,
4
- React.createElement("button", { className: 'str-chat__poll-action', onClick: openModal }, buttonText),
5
- React.createElement(Modal, { className: modalClassName, onClose: closeModal, open: modalIsOpen }, children)));
2
+ import { Modal as DefaultModal } from '../../Modal';
3
+ import { useComponentContext } from '../../../context';
4
+ export const PollAction = ({ buttonText, children, closeModal, modalClassName, modalIsOpen, openModal, }) => {
5
+ const { Modal = DefaultModal } = useComponentContext();
6
+ return (React.createElement(React.Fragment, null,
7
+ React.createElement("button", { className: 'str-chat__poll-action', onClick: openModal }, buttonText),
8
+ React.createElement(Modal, { className: modalClassName, onClose: closeModal, open: modalIsOpen }, children)));
9
+ };
@@ -1,3 +1,4 @@
1
+ import clsx from 'clsx';
1
2
  import React, { useCallback, useState } from 'react';
2
3
  import { PollAction } from './PollAction';
3
4
  import { AddCommentForm as DefaultAddCommentForm } from './AddCommentForm';
@@ -9,6 +10,7 @@ import { PollResults as DefaultPollResults } from './PollResults';
9
10
  import { MAX_OPTIONS_DISPLAYED, MAX_POLL_OPTIONS } from '../constants';
10
11
  import { useChannelStateContext, useChatContext, useMessageContext, usePollContext, useTranslationContext, } from '../../../context';
11
12
  import { useStateStore } from '../../../store';
13
+ const COMMON_MODAL_CLASS = 'str-chat__poll-action-modal';
12
14
  const pollStateSelector = (nextValue) => ({
13
15
  allow_answers: nextValue.allow_answers,
14
16
  allow_user_suggested_options: nextValue.allow_user_suggested_options,
@@ -31,18 +33,18 @@ export const PollActions = ({ AddCommentForm = DefaultAddCommentForm, EndPollDia
31
33
  return (React.createElement("div", { className: 'str-chat__poll-actions' },
32
34
  options.length > MAX_OPTIONS_DISPLAYED && (React.createElement(PollAction, { buttonText: t('See all options ({{count}})', {
33
35
  count: options.length,
34
- }), closeModal: closeModal, modalIsOpen: modalOpen === 'view-all-options', openModal: () => setModalOpen('view-all-options') },
36
+ }), closeModal: closeModal, modalClassName: COMMON_MODAL_CLASS, modalIsOpen: modalOpen === 'view-all-options', openModal: () => setModalOpen('view-all-options') },
35
37
  React.createElement(PollOptionsFullList, { close: closeModal }))),
36
38
  !is_closed &&
37
39
  allow_user_suggested_options &&
38
- options.length < MAX_POLL_OPTIONS && (React.createElement(PollAction, { buttonText: t('Suggest an option'), closeModal: closeModal, modalClassName: 'str-chat__suggest-poll-option-modal', modalIsOpen: modalOpen === 'suggest-option', openModal: () => setModalOpen('suggest-option') },
40
+ options.length < MAX_POLL_OPTIONS && (React.createElement(PollAction, { buttonText: t('Suggest an option'), closeModal: closeModal, modalClassName: clsx(COMMON_MODAL_CLASS, 'str-chat__suggest-poll-option-modal'), modalIsOpen: modalOpen === 'suggest-option', openModal: () => setModalOpen('suggest-option') },
39
41
  React.createElement(SuggestPollOptionForm, { close: closeModal, messageId: message.id }))),
40
- !is_closed && allow_answers && (React.createElement(PollAction, { buttonText: ownAnswer ? t('Update your comment') : t('Add a comment'), closeModal: closeModal, modalClassName: 'str-chat__add-poll-answer-modal', modalIsOpen: modalOpen === 'add-comment', openModal: () => setModalOpen('add-comment') },
42
+ !is_closed && allow_answers && (React.createElement(PollAction, { buttonText: ownAnswer ? t('Update your comment') : t('Add a comment'), closeModal: closeModal, modalClassName: clsx(COMMON_MODAL_CLASS, 'str-chat__add-poll-answer-modal'), modalIsOpen: modalOpen === 'add-comment', openModal: () => setModalOpen('add-comment') },
41
43
  React.createElement(AddCommentForm, { close: closeModal, messageId: message.id }))),
42
- answers_count > 0 && channelCapabilities['query-poll-votes'] && (React.createElement(PollAction, { buttonText: t('View {{count}} comments', { count: answers_count }), closeModal: closeModal, modalClassName: 'str-chat__poll-answer-list-modal', modalIsOpen: modalOpen === 'view-comments', openModal: () => setModalOpen('view-comments') },
44
+ answers_count > 0 && channelCapabilities['query-poll-votes'] && (React.createElement(PollAction, { buttonText: t('View {{count}} comments', { count: answers_count }), closeModal: closeModal, modalClassName: clsx(COMMON_MODAL_CLASS, 'str-chat__poll-answer-list-modal'), modalIsOpen: modalOpen === 'view-comments', openModal: () => setModalOpen('view-comments') },
43
45
  React.createElement(PollAnswerList, { close: closeModal, onUpdateOwnAnswerClick: onUpdateAnswerClick }))),
44
- React.createElement(PollAction, { buttonText: t('View results'), closeModal: closeModal, modalClassName: 'str-chat__poll-results-modal', modalIsOpen: modalOpen === 'view-results', openModal: () => setModalOpen('view-results') },
46
+ React.createElement(PollAction, { buttonText: t('View results'), closeModal: closeModal, modalClassName: clsx(COMMON_MODAL_CLASS, 'str-chat__poll-results-modal'), modalIsOpen: modalOpen === 'view-results', openModal: () => setModalOpen('view-results') },
45
47
  React.createElement(PollResults, { close: closeModal })),
46
- !is_closed && created_by_id === client.user?.id && (React.createElement(PollAction, { buttonText: t('End vote'), closeModal: closeModal, modalClassName: 'str-chat__end-poll-modal', modalIsOpen: modalOpen === 'end-vote', openModal: () => setModalOpen('end-vote') },
48
+ !is_closed && created_by_id === client.user?.id && (React.createElement(PollAction, { buttonText: t('End vote'), closeModal: closeModal, modalClassName: clsx(COMMON_MODAL_CLASS, 'str-chat__end-poll-modal'), modalIsOpen: modalOpen === 'end-vote', openModal: () => setModalOpen('end-vote') },
47
49
  React.createElement(EndPollDialog, { close: closeModal })))));
48
50
  };
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
2
  import type { ReactionDetailsComparator, ReactionSummary, ReactionType } from './types';
3
+ import type { ReactionSort } from 'stream-chat';
3
4
  import type { ModalProps } from '../Modal';
4
5
  import type { MessageContextValue } from '../../context';
5
- import type { ReactionSort } from 'stream-chat';
6
6
  export type ReactionsListModalProps = ModalProps & Partial<Pick<MessageContextValue, 'handleFetchReactions' | 'reactionDetailsSort'>> & {
7
7
  reactions: ReactionSummary[];
8
8
  selectedReactionType: ReactionType;
@@ -1,12 +1,13 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import clsx from 'clsx';
3
- import { Modal } from '../Modal';
3
+ import { Modal as DefaultModal } from '../Modal';
4
4
  import { useFetchReactions } from './hooks/useFetchReactions';
5
5
  import { LoadingIndicator } from '../Loading';
6
6
  import { Avatar } from '../Avatar';
7
- import { useMessageContext } from '../../context';
7
+ import { useComponentContext, useMessageContext } from '../../context';
8
8
  const defaultReactionDetailsSort = { created_at: -1 };
9
9
  export function ReactionsListModal({ handleFetchReactions, onSelectedReactionTypeChange, reactionDetailsSort: propReactionDetailsSort, reactions, selectedReactionType, sortReactionDetails: propSortReactionDetails, ...modalProps }) {
10
+ const { Modal = DefaultModal } = useComponentContext();
10
11
  const selectedReaction = reactions.find(({ reactionType }) => reactionType === selectedReactionType);
11
12
  const SelectedEmojiComponent = selectedReaction?.EmojiComponent ?? null;
12
13
  const { reactionDetailsSort: contextReactionDetailsSort, sortReactionDetails: contextSortReactionDetails, } = useMessageContext('ReactionsListModal');
@@ -1,5 +1,5 @@
1
1
  import clsx from 'clsx';
2
- import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
3
3
  import Textarea from 'react-textarea-autosize';
4
4
  import { useMessageComposer } from '../MessageInput';
5
5
  import { useComponentContext, useMessageInputContext, useTranslationContext, } from '../../context';
@@ -116,7 +116,6 @@ export const TextareaComposer = ({ className, closeSuggestionsOnClickOutside, co
116
116
  event.preventDefault();
117
117
  }
118
118
  handleSubmit();
119
- textareaRef.current.selectionEnd = 0;
120
119
  }
121
120
  }, [
122
121
  focusedItemIndex,
@@ -135,23 +134,13 @@ export const TextareaComposer = ({ className, closeSuggestionsOnClickOutside, co
135
134
  textComposer.closeSuggestions();
136
135
  }
137
136
  }, [onScroll, textComposer]);
138
- const setSelectionDebounced = useCallback((e) => {
137
+ const setSelection = useCallback((e) => {
139
138
  onSelect?.(e);
140
139
  textComposer.setSelection({
141
140
  end: e.target.selectionEnd,
142
141
  start: e.target.selectionStart,
143
142
  });
144
143
  }, [onSelect, textComposer]);
145
- useEffect(() => {
146
- // FIXME: find the real reason for cursor being set to the end on each change
147
- // This is a workaround to prevent the cursor from jumping
148
- // to the end of the textarea when the user is typing
149
- // at the position that is not at the end of the textarea value.
150
- if (textareaRef.current && !isComposing) {
151
- textareaRef.current.selectionStart = selection.start;
152
- textareaRef.current.selectionEnd = selection.end;
153
- }
154
- }, [text, textareaRef, selection.start, selection.end, isComposing]);
155
144
  useEffect(() => {
156
145
  if (textComposer.suggestions) {
157
146
  setFocusedItemIndex(0);
@@ -163,24 +152,26 @@ export const TextareaComposer = ({ className, closeSuggestionsOnClickOutside, co
163
152
  return;
164
153
  textareaRef.current.focus();
165
154
  }, [attachments, focus, quotedMessage, textareaRef]);
166
- useEffect(() => {
155
+ useLayoutEffect(() => {
167
156
  /**
168
- * The textarea value has to be overridden outside the render cycle so that the events like compositionend can be triggered.
169
- * If we have overridden the value during the component rendering, the compositionend event would not be triggered, and
170
- * it would not be possible to type composed characters (ô).
171
- * On the other hand, just removing the value override via prop (value={text}) would not allow us to change the text based on
172
- * middleware results (e.g. replace characters with emojis)
157
+ * It is important to perform set text and after that the range
158
+ * to prevent cursor reset to the end of the textarea if doing it in separate effects.
173
159
  */
174
160
  const textarea = textareaRef.current;
175
- if (!textarea)
161
+ if (!textarea || isComposing)
162
+ return;
163
+ const length = textarea.value.length;
164
+ const start = Math.max(0, Math.min(selection.start, length));
165
+ const end = Math.max(start, Math.min(selection.end, length));
166
+ if (textarea.selectionStart === start && textarea.selectionEnd === end)
176
167
  return;
177
- textarea.value = text;
178
- }, [textareaRef, text]);
168
+ textarea.setSelectionRange(start, end, 'forward');
169
+ }, [text, selection.start, selection.end, isComposing, textareaRef]);
179
170
  return (React.createElement("div", { className: clsx('rta', 'str-chat__textarea str-chat__message-textarea-react-host', containerClassName, {
180
171
  ['rta--loading']: isLoadingItems,
181
172
  }), ref: containerRef },
182
- React.createElement(Textarea, { ...additionalTextareaProps, ...restTextareaProps, "aria-label": cooldownRemaining ? t('Slow Mode ON') : placeholder, className: clsx('rta__textarea', 'str-chat__textarea__textarea str-chat__message-textarea', className), "data-testid": 'message-input', disabled: !enabled || !!cooldownRemaining, maxRows: maxRows, minRows: minRows, onBlur: onBlur, onChange: changeHandler, onCompositionEnd: onCompositionEnd, onCompositionStart: onCompositionStart, onKeyDown: keyDownHandler, onPaste: onPaste, onScroll: scrollHandler, onSelect: setSelectionDebounced, placeholder: placeholder || t('Type your message'), ref: (ref) => {
173
+ React.createElement(Textarea, { ...additionalTextareaProps, ...restTextareaProps, "aria-label": cooldownRemaining ? t('Slow Mode ON') : placeholder, className: clsx('rta__textarea', 'str-chat__textarea__textarea str-chat__message-textarea', className), "data-testid": 'message-input', disabled: !enabled || !!cooldownRemaining, maxRows: maxRows, minRows: minRows, onBlur: onBlur, onChange: changeHandler, onCompositionEnd: onCompositionEnd, onCompositionStart: onCompositionStart, onKeyDown: keyDownHandler, onPaste: onPaste, onScroll: scrollHandler, onSelect: setSelection, placeholder: placeholder || t('Type your message'), ref: (ref) => {
183
174
  textareaRef.current = ref;
184
- } }),
175
+ }, value: text }),
185
176
  !isComposing && (React.createElement(AutocompleteSuggestionList, { className: listClassName, closeOnClickOutside: closeSuggestionsOnClickOutside, focusedItemIndex: focusedItemIndex, setFocusedItemIndex: setFocusedItemIndex }))));
186
177
  };
@@ -1,6 +1,6 @@
1
1
  import type { PropsWithChildren } from 'react';
2
2
  import React from 'react';
3
- import type { AttachmentPreviewListProps, AttachmentProps, AvatarProps, BaseImageProps, ChannelPreviewActionButtonsProps, CooldownTimerProps, CustomMessageActionsListProps, DateSeparatorProps, EmojiSearchIndex, EmptyStateIndicatorProps, EventComponentProps, FixedHeightMessageProps, GiphyPreviewMessageProps, LoadingIndicatorProps, MessageBouncePromptProps, MessageDeletedProps, MessageInputProps, MessageListNotificationsProps, MessageNotificationProps, MessageOptionsProps, MessageProps, MessageRepliesCountButtonProps, MessageStatusProps, MessageTimestampProps, MessageUIComponentProps, ModalGalleryProps, PinIndicatorProps, PollCreationDialogProps, PollOptionSelectorProps, QuotedMessagePreviewProps, ReactionOptions, ReactionSelectorProps, ReactionsListModalProps, ReactionsListProps, RecordingPermissionDeniedNotificationProps, ReminderNotificationProps, SendButtonProps, StartRecordingAudioButtonProps, StreamedMessageTextProps, TextareaComposerProps, ThreadHeaderProps, ThreadListItemProps, ThreadListItemUIProps, TimestampProps, TypingIndicatorProps, UnreadMessagesNotificationProps, UnreadMessagesSeparatorProps } from '../components';
3
+ import type { AttachmentPreviewListProps, AttachmentProps, AvatarProps, BaseImageProps, ChannelPreviewActionButtonsProps, CooldownTimerProps, CustomMessageActionsListProps, DateSeparatorProps, EmojiSearchIndex, EmptyStateIndicatorProps, EventComponentProps, FixedHeightMessageProps, GiphyPreviewMessageProps, LoadingIndicatorProps, MessageBouncePromptProps, MessageDeletedProps, MessageInputProps, MessageListNotificationsProps, MessageNotificationProps, MessageOptionsProps, MessageProps, MessageRepliesCountButtonProps, MessageStatusProps, MessageTimestampProps, MessageUIComponentProps, ModalGalleryProps, ModalProps, PinIndicatorProps, PollCreationDialogProps, PollOptionSelectorProps, QuotedMessagePreviewProps, ReactionOptions, ReactionSelectorProps, ReactionsListModalProps, ReactionsListProps, RecordingPermissionDeniedNotificationProps, ReminderNotificationProps, SendButtonProps, StartRecordingAudioButtonProps, StreamedMessageTextProps, TextareaComposerProps, ThreadHeaderProps, ThreadListItemProps, ThreadListItemUIProps, TimestampProps, TypingIndicatorProps, UnreadMessagesNotificationProps, UnreadMessagesSeparatorProps } from '../components';
4
4
  import type { SuggestionItemProps, SuggestionListProps } from '../components/TextareaComposer';
5
5
  import type { SearchProps, SearchResultsPresearchProps, SearchSourceResultListProps } from '../experimental';
6
6
  import type { PropsWithChildrenOnly, UnknownType } from '../types/types';
@@ -87,6 +87,8 @@ export type ComponentContextValue = {
87
87
  MessageSystem?: React.ComponentType<EventComponentProps>;
88
88
  /** Custom UI component to display a timestamp on a message, defaults to and accepts same props as: [MessageTimestamp](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageTimestamp.tsx) */
89
89
  MessageTimestamp?: React.ComponentType<MessageTimestampProps>;
90
+ /** Custom UI component for viewing content in a modal, defaults to and accepts the same props as [Modal](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Modal/Modal.tsx) */
91
+ Modal?: React.ComponentType<ModalProps>;
90
92
  /** Custom UI component for viewing message's image attachments, defaults to and accepts the same props as [ModalGallery](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Gallery/ModalGallery.tsx) */
91
93
  ModalGallery?: React.ComponentType<ModalGalleryProps>;
92
94
  /** Custom UI component to override default pinned message indicator, defaults to and accepts same props as: [PinIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/icons.tsx) */