Skip to content

Commit fea0186

Browse files
committed
feat: add support for global modal rendering at the top of the chat component tree
1 parent 93f0a4b commit fea0186

File tree

17 files changed

+272
-49
lines changed

17 files changed

+272
-49
lines changed

src/components/Channel/Channel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ type ChannelPropsForwardedToComponentContext = Pick<
128128
| 'MessageStatus'
129129
| 'MessageSystem'
130130
| 'MessageTimestamp'
131+
| 'Modal'
131132
| 'ModalGallery'
132133
| 'PinIndicator'
133134
| 'PollActions'
@@ -1221,6 +1222,7 @@ const ChannelInner = (
12211222
MessageStatus: props.MessageStatus,
12221223
MessageSystem: props.MessageSystem,
12231224
MessageTimestamp: props.MessageTimestamp,
1225+
Modal: props.Modal,
12241226
ModalGallery: props.ModalGallery,
12251227
PinIndicator: props.PinIndicator,
12261228
PollActions: props.PollActions,
@@ -1288,6 +1290,7 @@ const ChannelInner = (
12881290
props.MessageStatus,
12891291
props.MessageSystem,
12901292
props.MessageTimestamp,
1293+
props.Modal,
12911294
props.ModalGallery,
12921295
props.PinIndicator,
12931296
props.PollActions,

src/components/Chat/Chat.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { useChannelsQueryState } from './hooks/useChannelsQueryState';
1414
import { ChatProvider } from '../../context/ChatContext';
1515
import { TranslationProvider } from '../../context/TranslationContext';
1616
import type { CustomClasses } from '../../context/ChatContext';
17-
import type { MessageContextValue } from '../../context';
17+
import { type MessageContextValue, ModalDialogManagerProvider } from '../../context';
1818
import type { SupportedTranslations } from '../../i18n/types';
1919
import type { Streami18n } from '../../i18n/Streami18n';
2020

@@ -110,7 +110,9 @@ export const Chat = (props: PropsWithChildren<ChatProps>) => {
110110

111111
return (
112112
<ChatProvider value={chatContextValue}>
113-
<TranslationProvider value={translators}>{children}</TranslationProvider>
113+
<TranslationProvider value={translators}>
114+
<ModalDialogManagerProvider>{children}</ModalDialogManagerProvider>
115+
</TranslationProvider>
114116
</ChatProvider>
115117
);
116118
};

src/components/Dialog/DialogManager.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { nanoid } from 'nanoid';
22
import { StateStore } from 'stream-chat';
33

4-
export type GetOrCreateDialogParams = {
4+
export type GetDialogParams = {
55
id: DialogId;
66
};
7+
export type GetOrCreateDialogParams = GetDialogParams;
78

89
type DialogId = string;
910

@@ -57,6 +58,10 @@ export class DialogManager {
5758
);
5859
}
5960

61+
get(id: DialogId) {
62+
return this.state.getLatestValue().dialogsById[id];
63+
}
64+
6065
getOrCreate({ id }: GetOrCreateDialogParams) {
6166
let dialog = this.state.getLatestValue().dialogsById[id];
6267
if (!dialog) {

src/components/Dialog/DialogPortal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export const DialogPortalEntry = ({
3131
children,
3232
dialogId,
3333
}: PropsWithChildren<DialogPortalEntryProps>) => {
34-
const { dialogManager } = useDialogManager();
35-
const dialogIsOpen = useDialogIsOpen(dialogId);
34+
const { dialogManager } = useDialogManager({ dialogId });
35+
const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager.id);
3636

3737
const getPortalDestination = useCallback(
3838
() => document.querySelector(`div[data-str-chat__portal-id="${dialogManager.id}"]`),

src/components/Dialog/hooks/useDialog.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { useCallback, useEffect } from 'react';
2-
import { useDialogManager } from '../../../context';
2+
import { modalDialogManagerId, useDialogManager } from '../../../context';
33
import { useStateStore } from '../../../store';
44

55
import type { DialogManagerState, GetOrCreateDialogParams } from '../DialogManager';
66

7-
export const useDialog = ({ id }: GetOrCreateDialogParams) => {
8-
const { dialogManager } = useDialogManager();
7+
export type UseDialogParams = GetOrCreateDialogParams & {
8+
dialogManagerId?: string;
9+
};
10+
11+
export const useDialog = ({ dialogManagerId, id }: UseDialogParams) => {
12+
const { dialogManager } = useDialogManager({ dialogManagerId });
913

1014
useEffect(
1115
() => () => {
@@ -21,15 +25,23 @@ export const useDialog = ({ id }: GetOrCreateDialogParams) => {
2125
return dialogManager.getOrCreate({ id });
2226
};
2327

24-
export const useDialogIsOpen = (id: string) => {
25-
const { dialogManager } = useDialogManager();
28+
export const modalDialogId = 'modal-dialog' as const;
29+
30+
export const useModalDialog = () =>
31+
useDialog({ dialogManagerId: modalDialogManagerId, id: modalDialogId });
32+
33+
export const useDialogIsOpen = (id: string, dialogManagerId?: string) => {
34+
const { dialogManager } = useDialogManager({ dialogManagerId });
2635
const dialogIsOpenSelector = useCallback(
2736
({ dialogsById }: DialogManagerState) => ({ isOpen: !!dialogsById[id]?.isOpen }),
2837
[id],
2938
);
3039
return useStateStore(dialogManager.state, dialogIsOpenSelector).isOpen;
3140
};
3241

42+
export const useModalDialogIsOpen = () =>
43+
useDialogIsOpen(modalDialogId, modalDialogManagerId);
44+
3345
const openedDialogCountSelector = (nextValue: DialogManagerState) => ({
3446
openedDialogCount: Object.values(nextValue.dialogsById).reduce((count, dialog) => {
3547
if (dialog.isOpen) return count + 1;

src/components/Gallery/Gallery.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { sanitizeUrl } from '@braintree/sanitize-url';
44
import clsx from 'clsx';
55

66
import { BaseImage as DefaultBaseImage } from './BaseImage';
7-
import { Modal } from '../Modal';
7+
import { Modal as DefaultModal } from '../Modal';
88
import { ModalGallery as DefaultModalGallery } from './ModalGallery';
99

1010
import { useComponentContext } from '../../context/ComponentContext';
@@ -29,8 +29,11 @@ const UnMemoizedGallery = (props: GalleryProps) => {
2929
const [index, setIndex] = useState(0);
3030
const [modalOpen, setModalOpen] = useState(false);
3131

32-
const { BaseImage = DefaultBaseImage, ModalGallery = DefaultModalGallery } =
33-
useComponentContext('Gallery');
32+
const {
33+
BaseImage = DefaultBaseImage,
34+
Modal = DefaultModal,
35+
ModalGallery = DefaultModalGallery,
36+
} = useComponentContext('Gallery');
3437
const { t } = useTranslationContext('Gallery');
3538

3639
const imageFallbackTitle = t('User uploaded content');

src/components/Gallery/Image.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { CSSProperties, MutableRefObject } from 'react';
2+
import { useCallback } from 'react';
23
import React, { useState } from 'react';
34
import { sanitizeUrl } from '@braintree/sanitize-url';
45

56
import { BaseImage as DefaultBaseImage } from './BaseImage';
6-
import { Modal } from '../Modal';
7+
import { Modal as DefaultModal } from '../Modal';
78
import { ModalGallery as DefaultModalGallery } from './ModalGallery';
89
import { useComponentContext } from '../../context';
910

@@ -42,28 +43,36 @@ export const ImageComponent = (props: ImageProps) => {
4243
} = props;
4344

4445
const [modalIsOpen, setModalIsOpen] = useState(false);
45-
const { BaseImage = DefaultBaseImage, ModalGallery = DefaultModalGallery } =
46-
useComponentContext('ImageComponent');
46+
const {
47+
BaseImage = DefaultBaseImage,
48+
Modal = DefaultModal,
49+
ModalGallery = DefaultModalGallery,
50+
} = useComponentContext('ImageComponent');
4751

4852
const imageSrc = sanitizeUrl(previewUrl || image_url || thumb_url);
53+
const closeModal = useCallback(() => {
54+
setModalIsOpen(false);
55+
}, []);
4956

50-
const toggleModal = () => setModalIsOpen((modalIsOpen) => !modalIsOpen);
57+
const openModal = useCallback(() => {
58+
setModalIsOpen(true);
59+
}, []);
5160

5261
return (
5362
<>
5463
<BaseImage
5564
alt={fallback}
5665
className='str-chat__message-attachment--img'
5766
data-testid='image-test'
58-
onClick={toggleModal}
67+
onClick={openModal}
5968
src={imageSrc}
6069
style={style}
6170
tabIndex={0}
6271
title={fallback}
6372
{...dimensions}
6473
{...(innerRef && { ref: innerRef })}
6574
/>
66-
<Modal className='str-chat__image-modal' onClose={toggleModal} open={modalIsOpen}>
75+
<Modal className='str-chat__image-modal' onClose={closeModal} open={modalIsOpen}>
6776
<ModalGallery images={[props]} index={0} />
6877
</Modal>
6978
</>

src/components/MessageBounce/MessageBounceModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { ComponentType, PropsWithChildren } from 'react';
22
import React from 'react';
33
import type { ModalProps } from '../Modal';
4-
import { Modal } from '../Modal';
5-
import { MessageBounceProvider } from '../../context';
4+
import { Modal as DefaultModal } from '../Modal';
5+
import { MessageBounceProvider, useComponentContext } from '../../context';
66
import type { MessageBouncePromptProps } from './MessageBouncePrompt';
77

88
export type MessageBounceModalProps = PropsWithChildren<
@@ -15,6 +15,7 @@ export function MessageBounceModal({
1515
MessageBouncePrompt,
1616
...modalProps
1717
}: MessageBounceModalProps) {
18+
const { Modal = DefaultModal } = useComponentContext();
1819
return (
1920
<Modal className='str-chat__message-bounce-modal' {...modalProps}>
2021
<MessageBounceProvider>

src/components/MessageInput/AttachmentSelector.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useAttachmentManagerState } from './hooks/useAttachmentManagerState';
44
import { CHANNEL_CONTAINER_ID } from '../Channel/constants';
55
import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
66
import { DialogMenuButton } from '../Dialog/DialogMenu';
7-
import { Modal } from '../Modal';
7+
import { Modal as DefaultModal } from '../Modal';
88
import { ShareLocationDialog as DefaultLocationDialog } from '../Location';
99
import { PollCreationDialog as DefaultPollCreationDialog } from '../Poll';
1010
import { Portal } from '../Portal/Portal';
@@ -192,6 +192,7 @@ export const AttachmentSelector = ({
192192
getModalPortalDestination,
193193
}: AttachmentSelectorProps) => {
194194
const { t } = useTranslationContext();
195+
const { Modal = DefaultModal } = useComponentContext();
195196
const { channelCapabilities } = useChannelStateContext();
196197
const messageComposer = useMessageComposer();
197198

src/components/MessageInput/EditMessageForm.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useCallback, useEffect } from 'react';
22
import { MessageInput } from './MessageInput';
33
import { MessageInputFlat } from './MessageInputFlat';
4-
import { Modal } from '../Modal';
4+
import { Modal as DefaultModal } from '../Modal';
55
import {
66
useComponentContext,
77
useMessageContext,
@@ -71,7 +71,8 @@ export const EditMessageForm = () => {
7171
export const EditMessageModal = ({
7272
additionalMessageInputProps,
7373
}: Pick<MessageUIComponentProps, 'additionalMessageInputProps'>) => {
74-
const { EditMessageInput = EditMessageForm } = useComponentContext();
74+
const { EditMessageInput = EditMessageForm, Modal = DefaultModal } =
75+
useComponentContext();
7576
const { clearEditingState } = useMessageContext();
7677
const messageComposer = useMessageComposer();
7778
const onEditModalClose = useCallback(() => {

0 commit comments

Comments
 (0)