stream-chat-react 12.0.0-rc.13 → 12.0.0-rc.15

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 (73) hide show
  1. package/dist/components/Channel/hooks/useCreateChannelStateContext.js +1 -0
  2. package/dist/components/ChannelHeader/ChannelHeader.js +4 -5
  3. package/dist/components/ChannelPreview/hooks/useChannelPreviewInfo.js +14 -16
  4. package/dist/components/ChannelPreview/utils.js +9 -20
  5. package/dist/components/ChannelSearch/hooks/useChannelSearch.js +2 -3
  6. package/dist/components/ChatView/ChatView.js +2 -1
  7. package/dist/components/Dialog/DialogAnchor.d.ts +25 -0
  8. package/dist/components/Dialog/DialogAnchor.js +68 -0
  9. package/dist/components/Dialog/DialogManager.d.ts +43 -0
  10. package/dist/components/Dialog/DialogManager.js +98 -0
  11. package/dist/components/Dialog/DialogPortal.d.ts +7 -0
  12. package/dist/components/Dialog/DialogPortal.js +25 -0
  13. package/dist/components/Dialog/hooks/index.d.ts +1 -0
  14. package/dist/components/Dialog/hooks/index.js +1 -0
  15. package/dist/components/Dialog/hooks/useDialog.d.ts +4 -0
  16. package/dist/components/Dialog/hooks/useDialog.js +26 -0
  17. package/dist/components/Dialog/index.d.ts +4 -0
  18. package/dist/components/Dialog/index.js +4 -0
  19. package/dist/components/Message/Message.js +3 -5
  20. package/dist/components/Message/MessageOptions.d.ts +1 -2
  21. package/dist/components/Message/MessageOptions.js +13 -9
  22. package/dist/components/Message/MessageSimple.js +5 -14
  23. package/dist/components/Message/hooks/useReactionHandler.d.ts +1 -7
  24. package/dist/components/Message/hooks/useReactionHandler.js +1 -63
  25. package/dist/components/Message/utils.js +3 -0
  26. package/dist/components/MessageActions/MessageActions.d.ts +1 -2
  27. package/dist/components/MessageActions/MessageActions.js +13 -47
  28. package/dist/components/MessageActions/MessageActionsBox.d.ts +1 -1
  29. package/dist/components/MessageActions/MessageActionsBox.js +6 -6
  30. package/dist/components/MessageInput/hooks/useUserTrigger.js +0 -1
  31. package/dist/components/MessageList/MessageList.js +7 -5
  32. package/dist/components/MessageList/VirtualizedMessageList.js +39 -37
  33. package/dist/components/Reactions/ReactionSelector.d.ts +1 -1
  34. package/dist/components/Reactions/ReactionSelector.js +33 -24
  35. package/dist/components/Reactions/ReactionSelectorWithButton.d.ts +13 -0
  36. package/dist/components/Reactions/ReactionSelectorWithButton.js +22 -0
  37. package/dist/components/Reactions/ReactionsList.d.ts +0 -3
  38. package/dist/components/Thread/Thread.js +2 -1
  39. package/dist/components/Threads/ThreadList/ThreadList.js +1 -1
  40. package/dist/components/Threads/ThreadList/ThreadListItemUI.js +1 -1
  41. package/dist/components/Threads/ThreadList/ThreadListLoadingIndicator.js +1 -1
  42. package/dist/components/Threads/ThreadList/ThreadListUnseenThreadsBanner.js +1 -1
  43. package/dist/components/Threads/hooks/useThreadManagerState.js +1 -1
  44. package/dist/components/Threads/hooks/useThreadState.js +1 -1
  45. package/dist/components/Threads/index.d.ts +0 -1
  46. package/dist/components/Threads/index.js +0 -1
  47. package/dist/components/index.d.ts +1 -0
  48. package/dist/components/index.js +1 -0
  49. package/dist/context/DialogManagerContext.d.ts +10 -0
  50. package/dist/context/DialogManagerContext.js +14 -0
  51. package/dist/context/MessageContext.d.ts +2 -10
  52. package/dist/context/index.d.ts +1 -0
  53. package/dist/context/index.js +1 -0
  54. package/dist/css/v2/index.css +1 -1
  55. package/dist/css/v2/index.layout.css +1 -1
  56. package/dist/index.browser.cjs +2164 -2004
  57. package/dist/index.browser.cjs.map +4 -4
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.js +1 -0
  60. package/dist/index.node.cjs +2087 -1918
  61. package/dist/index.node.cjs.map +4 -4
  62. package/dist/scss/v2/Dialog/Dialog-layout.scss +8 -0
  63. package/dist/scss/v2/Message/Message-layout.scss +8 -0
  64. package/dist/scss/v2/MessageReactions/MessageReactionsSelector-layout.scss +16 -0
  65. package/dist/scss/v2/ThreadList/ThreadList-layout.scss +4 -1
  66. package/dist/scss/v2/index.layout.scss +1 -0
  67. package/dist/store/hooks/index.d.ts +1 -0
  68. package/dist/store/hooks/index.js +1 -0
  69. package/dist/store/index.d.ts +1 -0
  70. package/dist/store/index.js +1 -0
  71. package/package.json +2 -2
  72. /package/dist/{components/Threads → store}/hooks/useStateStore.d.ts +0 -0
  73. /package/dist/{components/Threads → store}/hooks/useStateStore.js +0 -0
@@ -67,6 +67,7 @@ export const useCreateChannelStateContext = (value) => {
67
67
  }),
68
68
  // eslint-disable-next-line react-hooks/exhaustive-deps
69
69
  [
70
+ channel.data?.name, // otherwise ChannelHeader will not be updated
70
71
  channelId,
71
72
  channelUnreadUiState,
72
73
  debounceURLEnrichmentMs,
@@ -5,7 +5,10 @@ import { useChannelPreviewInfo } from '../ChannelPreview/hooks/useChannelPreview
5
5
  import { useChannelStateContext } from '../../context/ChannelStateContext';
6
6
  import { useChatContext } from '../../context/ChatContext';
7
7
  import { useTranslationContext } from '../../context/TranslationContext';
8
- const UnMemoizedChannelHeader = (props) => {
8
+ /**
9
+ * The ChannelHeader component renders some basic information about a Channel.
10
+ */
11
+ export const ChannelHeader = (props) => {
9
12
  const { Avatar = DefaultAvatar, MenuIcon = DefaultMenuIcon, image: overrideImage, live, title: overrideTitle, } = props;
10
13
  const { channel, watcher_count } = useChannelStateContext('ChannelHeader');
11
14
  const { openMobileNav } = useChatContext('ChannelHeader');
@@ -35,7 +38,3 @@ const UnMemoizedChannelHeader = (props) => {
35
38
  ' ')),
36
39
  t('{{ watcherCount }} online', { watcherCount: watcher_count })))));
37
40
  };
38
- /**
39
- * The ChannelHeader component renders some basic information about a Channel.
40
- */
41
- export const ChannelHeader = React.memo(UnMemoizedChannelHeader);
@@ -3,26 +3,24 @@ import { getDisplayImage, getDisplayTitle } from '../utils';
3
3
  import { useChatContext } from '../../../context';
4
4
  export const useChannelPreviewInfo = (props) => {
5
5
  const { channel, overrideImage, overrideTitle } = props;
6
- const { client } = useChatContext('ChannelPreview');
7
- const [displayTitle, setDisplayTitle] = useState(getDisplayTitle(channel, client.user));
8
- const [displayImage, setDisplayImage] = useState(getDisplayImage(channel, client.user));
6
+ const { client } = useChatContext('useChannelPreviewInfo');
7
+ const [displayTitle, setDisplayTitle] = useState(() => overrideTitle || getDisplayTitle(channel, client.user));
8
+ const [displayImage, setDisplayImage] = useState(() => overrideImage || getDisplayImage(channel, client.user));
9
9
  useEffect(() => {
10
- const handleEvent = () => {
11
- setDisplayTitle((displayTitle) => {
12
- const newDisplayTitle = getDisplayTitle(channel, client.user);
13
- return displayTitle !== newDisplayTitle ? newDisplayTitle : displayTitle;
14
- });
15
- setDisplayImage((displayImage) => {
16
- const newDisplayImage = getDisplayImage(channel, client.user);
17
- return displayImage !== newDisplayImage ? newDisplayImage : displayImage;
18
- });
10
+ if (overrideTitle && overrideImage)
11
+ return;
12
+ const updateTitles = () => {
13
+ if (!overrideTitle)
14
+ setDisplayTitle(getDisplayTitle(channel, client.user));
15
+ if (!overrideImage)
16
+ setDisplayImage(getDisplayImage(channel, client.user));
19
17
  };
20
- client.on('user.updated', handleEvent);
18
+ updateTitles();
19
+ client.on('user.updated', updateTitles);
21
20
  return () => {
22
- client.off('user.updated', handleEvent);
21
+ client.off('user.updated', updateTitles);
23
22
  };
24
- // eslint-disable-next-line react-hooks/exhaustive-deps
25
- }, []);
23
+ }, [channel, channel.data, client, overrideImage, overrideTitle]);
26
24
  return {
27
25
  displayImage: overrideImage || displayImage,
28
26
  displayTitle: overrideTitle || displayTitle,
@@ -23,25 +23,14 @@ export const getLatestMessagePreview = (channel, t, userLanguage = 'en') => {
23
23
  }
24
24
  return t('Empty message...');
25
25
  };
26
- export const getDisplayTitle = (channel, currentUser) => {
27
- let title = channel.data?.name;
26
+ const getChannelDisplayInfo = (info, channel, currentUser) => {
27
+ if (channel.data?.[info])
28
+ return channel.data[info];
28
29
  const members = Object.values(channel.state.members);
29
- if (!title && members.length === 2) {
30
- const otherMember = members.find((member) => member.user?.id !== currentUser?.id);
31
- if (otherMember?.user?.name) {
32
- title = otherMember.user.name;
33
- }
34
- }
35
- return title;
36
- };
37
- export const getDisplayImage = (channel, currentUser) => {
38
- let image = channel.data?.image;
39
- const members = Object.values(channel.state.members);
40
- if (!image && members.length === 2) {
41
- const otherMember = members.find((member) => member.user?.id !== currentUser?.id);
42
- if (otherMember?.user?.image) {
43
- image = otherMember.user.image;
44
- }
45
- }
46
- return image;
30
+ if (members.length !== 2)
31
+ return;
32
+ const otherMember = members.find((member) => member.user?.id !== currentUser?.id);
33
+ return otherMember?.user?.[info];
47
34
  };
35
+ export const getDisplayTitle = (channel, currentUser) => getChannelDisplayInfo('name', channel, currentUser);
36
+ export const getDisplayImage = (channel, currentUser) => getChannelDisplayInfo('image', channel, currentUser);
@@ -97,13 +97,12 @@ export const useChannelSearch = ({ channelType = 'messaging', clearSearchOnClick
97
97
  // @ts-expect-error
98
98
  {
99
99
  $or: [{ id: { $autocomplete: text } }, { name: { $autocomplete: text } }],
100
- id: { $ne: client.userID },
101
100
  ...searchQueryParams?.userFilters?.filters,
102
101
  }, { id: 1, ...searchQueryParams?.userFilters?.sort }, { limit: 8, ...searchQueryParams?.userFilters?.options });
103
102
  if (!searchForChannels) {
104
103
  searchQueryPromiseInProgress.current = userQueryPromise;
105
104
  const { users } = await searchQueryPromiseInProgress.current;
106
- results = users;
105
+ results = users.filter((u) => u.id !== client.user?.id);
107
106
  }
108
107
  else {
109
108
  const channelQueryPromise = client.queryChannels(
@@ -117,7 +116,7 @@ export const useChannelSearch = ({ channelType = 'messaging', clearSearchOnClick
117
116
  userQueryPromise,
118
117
  ]);
119
118
  const [channels, { users }] = await searchQueryPromiseInProgress.current;
120
- results = [...channels, ...users];
119
+ results = [...channels, ...users.filter((u) => u.id !== client.user?.id)];
121
120
  }
122
121
  }
123
122
  catch (error) {
@@ -1,8 +1,9 @@
1
1
  import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
2
- import { ThreadProvider, useStateStore } from '../Threads';
2
+ import { ThreadProvider } from '../Threads';
3
3
  import { Icon } from '../Threads/icons';
4
4
  import { UnreadCountBadge } from '../Threads/UnreadCountBadge';
5
5
  import { useChatContext } from '../../context';
6
+ import { useStateStore } from '../../store';
6
7
  import clsx from 'clsx';
7
8
  const availableChatViews = ['channels', 'threads'];
8
9
  const ChatViewContext = createContext({
@@ -0,0 +1,25 @@
1
+ import { Placement } from '@popperjs/core';
2
+ import React, { ComponentProps, PropsWithChildren } from 'react';
3
+ export interface DialogAnchorOptions {
4
+ open: boolean;
5
+ placement: Placement;
6
+ referenceElement: HTMLElement | null;
7
+ }
8
+ export declare function useDialogAnchor<T extends HTMLElement>({ open, placement, referenceElement, }: DialogAnchorOptions): {
9
+ attributes: {
10
+ [key: string]: {
11
+ [key: string]: string;
12
+ } | undefined;
13
+ };
14
+ setPopperElement: React.Dispatch<React.SetStateAction<T | null>>;
15
+ styles: {
16
+ [key: string]: React.CSSProperties;
17
+ };
18
+ };
19
+ type DialogAnchorProps = PropsWithChildren<Partial<DialogAnchorOptions>> & {
20
+ id: string;
21
+ focus?: boolean;
22
+ trapFocus?: boolean;
23
+ } & ComponentProps<'div'>;
24
+ export declare const DialogAnchor: ({ children, className, focus, id, placement, referenceElement, trapFocus, ...restDivProps }: DialogAnchorProps) => React.JSX.Element | null;
25
+ export {};
@@ -0,0 +1,68 @@
1
+ import clsx from 'clsx';
2
+ import React, { useEffect, useState } from 'react';
3
+ import { FocusScope } from '@react-aria/focus';
4
+ import { usePopper } from 'react-popper';
5
+ import { DialogPortalEntry } from './DialogPortal';
6
+ import { useDialog, useDialogIsOpen } from './hooks';
7
+ export function useDialogAnchor({ open, placement, referenceElement, }) {
8
+ const [popperElement, setPopperElement] = useState(null);
9
+ const { attributes, styles, update } = usePopper(referenceElement, popperElement, {
10
+ modifiers: [
11
+ {
12
+ name: 'eventListeners',
13
+ options: {
14
+ // It's not safe to update popper position on resize and scroll, since popper's
15
+ // reference element might not be visible at the time.
16
+ resize: false,
17
+ scroll: false,
18
+ },
19
+ },
20
+ ],
21
+ placement,
22
+ });
23
+ useEffect(() => {
24
+ if (open && popperElement) {
25
+ // Since the popper's reference element might not be (and usually is not) visible
26
+ // all the time, it's safer to force popper update before showing it.
27
+ // update is non-null only if popperElement is non-null
28
+ update?.();
29
+ }
30
+ }, [open, popperElement, update]);
31
+ if (popperElement && !open) {
32
+ setPopperElement(null);
33
+ }
34
+ return {
35
+ attributes,
36
+ setPopperElement,
37
+ styles,
38
+ };
39
+ }
40
+ export const DialogAnchor = ({ children, className, focus = true, id, placement = 'auto', referenceElement = null, trapFocus, ...restDivProps }) => {
41
+ const dialog = useDialog({ id });
42
+ const open = useDialogIsOpen(id);
43
+ const { attributes, setPopperElement, styles } = useDialogAnchor({
44
+ open,
45
+ placement,
46
+ referenceElement,
47
+ });
48
+ useEffect(() => {
49
+ if (!open)
50
+ return;
51
+ const hideOnEscape = (event) => {
52
+ if (event.key !== 'Escape')
53
+ return;
54
+ dialog?.close();
55
+ };
56
+ document.addEventListener('keyup', hideOnEscape);
57
+ return () => {
58
+ document.removeEventListener('keyup', hideOnEscape);
59
+ };
60
+ }, [dialog, open]);
61
+ // prevent rendering the dialog contents if the dialog should not be open / shown
62
+ if (!open) {
63
+ return null;
64
+ }
65
+ return (React.createElement(DialogPortalEntry, { dialogId: id },
66
+ React.createElement(FocusScope, { autoFocus: focus, contain: trapFocus, restoreFocus: true },
67
+ React.createElement("div", { ...restDivProps, ...attributes.popper, className: clsx('str-chat__dialog-contents', className), "data-testid": 'str-chat__dialog-contents', ref: setPopperElement, style: styles.popper, tabIndex: 0 }, children))));
68
+ };
@@ -0,0 +1,43 @@
1
+ import { StateStore } from 'stream-chat';
2
+ export type GetOrCreateDialogParams = {
3
+ id: DialogId;
4
+ };
5
+ type DialogId = string;
6
+ export type Dialog = {
7
+ close: () => void;
8
+ id: DialogId;
9
+ isOpen: boolean | undefined;
10
+ open: (zIndex?: number) => void;
11
+ remove: () => void;
12
+ toggle: (closeAll?: boolean) => void;
13
+ };
14
+ export type DialogManagerOptions = {
15
+ id?: string;
16
+ };
17
+ type Dialogs = Record<DialogId, Dialog>;
18
+ export type DialogManagerState = {
19
+ dialogsById: Dialogs;
20
+ };
21
+ /**
22
+ * Keeps a map of Dialog objects.
23
+ * Dialog can be controlled via `Dialog` object retrieved using `useDialog()` hook.
24
+ * The hook returns an object with the following API:
25
+ *
26
+ * - `dialog.open()` - opens the dialog
27
+ * - `dialog.close()` - closes the dialog
28
+ * - `dialog.toggle()` - toggles the dialog open state. Accepts boolean argument closeAll. If enabled closes any other dialog that would be open.
29
+ * - `dialog.remove()` - removes the dialog object reference from the state (primarily for cleanup purposes)
30
+ */
31
+ export declare class DialogManager {
32
+ id: string;
33
+ state: StateStore<DialogManagerState>;
34
+ constructor({ id }?: DialogManagerOptions);
35
+ get openDialogCount(): number;
36
+ getOrCreate({ id }: GetOrCreateDialogParams): Dialog;
37
+ open(params: GetOrCreateDialogParams, closeRest?: boolean): void;
38
+ close(id: DialogId): void;
39
+ closeAll(): void;
40
+ toggle(params: GetOrCreateDialogParams, closeAll?: boolean): void;
41
+ remove(id: DialogId): void;
42
+ }
43
+ export {};
@@ -0,0 +1,98 @@
1
+ import { StateStore } from 'stream-chat';
2
+ /**
3
+ * Keeps a map of Dialog objects.
4
+ * Dialog can be controlled via `Dialog` object retrieved using `useDialog()` hook.
5
+ * The hook returns an object with the following API:
6
+ *
7
+ * - `dialog.open()` - opens the dialog
8
+ * - `dialog.close()` - closes the dialog
9
+ * - `dialog.toggle()` - toggles the dialog open state. Accepts boolean argument closeAll. If enabled closes any other dialog that would be open.
10
+ * - `dialog.remove()` - removes the dialog object reference from the state (primarily for cleanup purposes)
11
+ */
12
+ export class DialogManager {
13
+ constructor({ id } = {}) {
14
+ this.state = new StateStore({
15
+ dialogsById: {},
16
+ });
17
+ this.id = id ?? new Date().getTime().toString();
18
+ }
19
+ get openDialogCount() {
20
+ return Object.values(this.state.getLatestValue().dialogsById).reduce((count, dialog) => {
21
+ if (dialog.isOpen)
22
+ return count + 1;
23
+ return count;
24
+ }, 0);
25
+ }
26
+ getOrCreate({ id }) {
27
+ let dialog = this.state.getLatestValue().dialogsById[id];
28
+ if (!dialog) {
29
+ dialog = {
30
+ close: () => {
31
+ this.close(id);
32
+ },
33
+ id,
34
+ isOpen: false,
35
+ open: () => {
36
+ this.open({ id });
37
+ },
38
+ remove: () => {
39
+ this.remove(id);
40
+ },
41
+ toggle: (closeAll = false) => {
42
+ this.toggle({ id }, closeAll);
43
+ },
44
+ };
45
+ this.state.next((current) => ({
46
+ ...current,
47
+ ...{ dialogsById: { ...current.dialogsById, [id]: dialog } },
48
+ }));
49
+ }
50
+ return dialog;
51
+ }
52
+ open(params, closeRest) {
53
+ const dialog = this.getOrCreate(params);
54
+ if (dialog.isOpen)
55
+ return;
56
+ if (closeRest) {
57
+ this.closeAll();
58
+ }
59
+ this.state.next((current) => ({
60
+ ...current,
61
+ dialogsById: { ...current.dialogsById, [dialog.id]: { ...dialog, isOpen: true } },
62
+ }));
63
+ }
64
+ close(id) {
65
+ const dialog = this.state.getLatestValue().dialogsById[id];
66
+ if (!dialog?.isOpen)
67
+ return;
68
+ this.state.next((current) => ({
69
+ ...current,
70
+ dialogsById: { ...current.dialogsById, [dialog.id]: { ...dialog, isOpen: false } },
71
+ }));
72
+ }
73
+ closeAll() {
74
+ Object.values(this.state.getLatestValue().dialogsById).forEach((dialog) => dialog.close());
75
+ }
76
+ toggle(params, closeAll = false) {
77
+ if (this.state.getLatestValue().dialogsById[params.id]?.isOpen) {
78
+ this.close(params.id);
79
+ }
80
+ else {
81
+ this.open(params, closeAll);
82
+ }
83
+ }
84
+ remove(id) {
85
+ const state = this.state.getLatestValue();
86
+ const dialog = state.dialogsById[id];
87
+ if (!dialog)
88
+ return;
89
+ this.state.next((current) => {
90
+ const newDialogs = { ...current.dialogsById };
91
+ delete newDialogs[id];
92
+ return {
93
+ ...current,
94
+ dialogsById: newDialogs,
95
+ };
96
+ });
97
+ }
98
+ }
@@ -0,0 +1,7 @@
1
+ import React, { PropsWithChildren } from 'react';
2
+ export declare const DialogPortalDestination: () => React.JSX.Element;
3
+ type DialogPortalEntryProps = {
4
+ dialogId: string;
5
+ };
6
+ export declare const DialogPortalEntry: ({ children, dialogId, }: PropsWithChildren<DialogPortalEntryProps>) => React.ReactPortal | null;
7
+ export {};
@@ -0,0 +1,25 @@
1
+ import React, { useLayoutEffect, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useDialogIsOpen, useOpenedDialogCount } from './hooks';
4
+ import { useDialogManager } from '../../context';
5
+ export const DialogPortalDestination = () => {
6
+ const { dialogManager } = useDialogManager();
7
+ const openedDialogCount = useOpenedDialogCount();
8
+ 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
+ '--str-chat__dialog-overlay-height': openedDialogCount > 0 ? '100%' : '0',
10
+ } }));
11
+ };
12
+ export const DialogPortalEntry = ({ children, dialogId, }) => {
13
+ const { dialogManager } = useDialogManager();
14
+ const dialogIsOpen = useDialogIsOpen(dialogId);
15
+ const [portalDestination, setPortalDestination] = useState(null);
16
+ useLayoutEffect(() => {
17
+ const destination = document.querySelector(`div[data-str-chat__portal-id="${dialogManager.id}"]`);
18
+ if (!destination)
19
+ return;
20
+ setPortalDestination(destination);
21
+ }, [dialogManager, dialogIsOpen]);
22
+ if (!portalDestination)
23
+ return null;
24
+ return createPortal(children, portalDestination);
25
+ };
@@ -0,0 +1 @@
1
+ export * from './useDialog';
@@ -0,0 +1 @@
1
+ export * from './useDialog';
@@ -0,0 +1,4 @@
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;
@@ -0,0 +1,26 @@
1
+ import { useCallback, useEffect } from 'react';
2
+ import { useDialogManager } from '../../../context';
3
+ import { useStateStore } from '../../../store';
4
+ export const useDialog = ({ id }) => {
5
+ const { dialogManager } = useDialogManager();
6
+ useEffect(() => () => {
7
+ dialogManager.remove(id);
8
+ }, [dialogManager, id]);
9
+ return dialogManager.getOrCreate({ id });
10
+ };
11
+ export const useDialogIsOpen = (id) => {
12
+ const { dialogManager } = useDialogManager();
13
+ const dialogIsOpenSelector = useCallback(({ dialogsById }) => [!!dialogsById[id]?.isOpen], [id]);
14
+ return useStateStore(dialogManager.state, dialogIsOpenSelector)[0];
15
+ };
16
+ const openedDialogCountSelector = (nextValue) => [
17
+ Object.values(nextValue.dialogsById).reduce((count, dialog) => {
18
+ if (dialog.isOpen)
19
+ return count + 1;
20
+ return count;
21
+ }, 0),
22
+ ];
23
+ export const useOpenedDialogCount = () => {
24
+ const { dialogManager } = useDialogManager();
25
+ return useStateStore(dialogManager.state, openedDialogCountSelector)[0];
26
+ };
@@ -0,0 +1,4 @@
1
+ export * from './DialogAnchor';
2
+ export * from './DialogManager';
3
+ export * from './DialogPortal';
4
+ export * from './hooks';
@@ -0,0 +1,4 @@
1
+ export * from './DialogAnchor';
2
+ export * from './DialogManager';
3
+ export * from './DialogPortal';
4
+ export * from './hooks';
@@ -1,5 +1,5 @@
1
- import React, { useCallback, useMemo, useRef } from 'react';
2
- import { useActionHandler, useDeleteHandler, useEditHandler, useFlagHandler, useMarkUnreadHandler, useMentionsHandler, useMuteHandler, useOpenThreadHandler, usePinHandler, useReactionClick, useReactionHandler, useReactionsFetcher, useRetryHandler, useUserHandler, useUserRole, } from './hooks';
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { useActionHandler, useDeleteHandler, useEditHandler, useFlagHandler, useMarkUnreadHandler, useMentionsHandler, useMuteHandler, useOpenThreadHandler, usePinHandler, useReactionHandler, useReactionsFetcher, useRetryHandler, useUserHandler, useUserRole, } from './hooks';
3
3
  import { areMessagePropsEqual, getMessageActions, MESSAGE_ACTIONS } from './utils';
4
4
  import { MessageProvider, useChannelActionContext, useChannelStateContext, useChatContext, useComponentContext, } from '../../context';
5
5
  import { MessageSimple as DefaultMessage } from './MessageSimple';
@@ -76,7 +76,6 @@ export const Message = (props) => {
76
76
  const { closeReactionSelectorOnClick, disableQuotedMessages, getDeleteMessageErrorNotification, getFetchReactionsErrorNotification, getFlagMessageErrorNotification, getFlagMessageSuccessNotification, getMarkMessageUnreadErrorNotification, getMarkMessageUnreadSuccessNotification, getMuteUserErrorNotification, getMuteUserSuccessNotification, getPinMessageErrorNotification, message, onlySenderCanEdit = false, onMentionsClick: propOnMentionsClick, onMentionsHover: propOnMentionsHover, openThread: propOpenThread, pinPermissions, reactionDetailsSort, retrySendMessage: propRetrySendMessage, sortReactionDetails, sortReactions, } = props;
77
77
  const { addNotification } = useChannelActionContext('Message');
78
78
  const { highlightedMessageId, mutes } = useChannelStateContext('Message');
79
- const reactionSelectorRef = useRef(null);
80
79
  const handleAction = useActionHandler(message);
81
80
  const handleOpenThread = useOpenThreadHandler(message, propOpenThread);
82
81
  const handleReaction = useReactionHandler(message);
@@ -113,7 +112,6 @@ export const Message = (props) => {
113
112
  getErrorNotification: getPinMessageErrorNotification,
114
113
  notify: addNotification,
115
114
  });
116
- const { isReactionEnabled, onReactionListClick, showDetailedReactions } = useReactionClick(message, reactionSelectorRef, undefined, closeReactionSelectorOnClick);
117
115
  const highlighted = highlightedMessageId === message.id;
118
- return (React.createElement(MemoizedMessage, { additionalMessageInputProps: props.additionalMessageInputProps, autoscrollToBottom: props.autoscrollToBottom, canPin: canPin, customMessageActions: props.customMessageActions, disableQuotedMessages: props.disableQuotedMessages, endOfGroup: props.endOfGroup, firstOfGroup: props.firstOfGroup, formatDate: props.formatDate, groupedByUser: props.groupedByUser, groupStyles: props.groupStyles, handleAction: handleAction, handleDelete: handleDelete, handleFetchReactions: handleFetchReactions, handleFlag: handleFlag, handleMarkUnread: handleMarkUnread, handleMute: handleMute, handleOpenThread: handleOpenThread, handlePin: handlePin, handleReaction: handleReaction, handleRetry: handleRetry, highlighted: highlighted, initialMessage: props.initialMessage, isReactionEnabled: isReactionEnabled, lastReceivedId: props.lastReceivedId, message: message, Message: props.Message, messageActions: props.messageActions, messageListRect: props.messageListRect, mutes: mutes, onMentionsClickMessage: onMentionsClick, onMentionsHoverMessage: onMentionsHover, onReactionListClick: onReactionListClick, onUserClick: props.onUserClick, onUserHover: props.onUserHover, pinPermissions: props.pinPermissions, reactionDetailsSort: reactionDetailsSort, reactionSelectorRef: reactionSelectorRef, readBy: props.readBy, renderText: props.renderText, showDetailedReactions: showDetailedReactions, sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: props.threadList, unsafeHTML: props.unsafeHTML, userRoles: userRoles }));
116
+ return (React.createElement(MemoizedMessage, { additionalMessageInputProps: props.additionalMessageInputProps, autoscrollToBottom: props.autoscrollToBottom, canPin: canPin, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: props.customMessageActions, disableQuotedMessages: props.disableQuotedMessages, endOfGroup: props.endOfGroup, firstOfGroup: props.firstOfGroup, formatDate: props.formatDate, groupedByUser: props.groupedByUser, groupStyles: props.groupStyles, handleAction: handleAction, handleDelete: handleDelete, handleFetchReactions: handleFetchReactions, handleFlag: handleFlag, handleMarkUnread: handleMarkUnread, handleMute: handleMute, handleOpenThread: handleOpenThread, handlePin: handlePin, handleReaction: handleReaction, handleRetry: handleRetry, highlighted: highlighted, initialMessage: props.initialMessage, lastReceivedId: props.lastReceivedId, message: message, Message: props.Message, messageActions: props.messageActions, messageListRect: props.messageListRect, mutes: mutes, onMentionsClickMessage: onMentionsClick, onMentionsHoverMessage: onMentionsHover, onUserClick: props.onUserClick, onUserHover: props.onUserHover, pinPermissions: props.pinPermissions, reactionDetailsSort: reactionDetailsSort, readBy: props.readBy, renderText: props.renderText, sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: props.threadList, unsafeHTML: props.unsafeHTML, userRoles: userRoles }));
119
117
  };
@@ -1,10 +1,9 @@
1
1
  import React from 'react';
2
- import { MessageContextValue } from '../../context/MessageContext';
3
2
  import type { DefaultStreamChatGenerics, IconProps } from '../../types/types';
3
+ import type { MessageContextValue } from '../../context/MessageContext';
4
4
  export type MessageOptionsProps<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = Partial<Pick<MessageContextValue<StreamChatGenerics>, 'handleOpenThread'>> & {
5
5
  ActionsIcon?: React.ComponentType<IconProps>;
6
6
  displayReplies?: boolean;
7
- messageWrapperRef?: React.RefObject<HTMLDivElement>;
8
7
  ReactionIcon?: React.ComponentType<IconProps>;
9
8
  theme?: string;
10
9
  ThreadIcon?: React.ComponentType<IconProps>;
@@ -1,13 +1,17 @@
1
+ import clsx from 'clsx';
1
2
  import React from 'react';
2
3
  import { ActionsIcon as DefaultActionsIcon, ReactionIcon as DefaultReactionIcon, ThreadIcon as DefaultThreadIcon, } from './icons';
3
4
  import { MESSAGE_ACTIONS } from './utils';
4
5
  import { MessageActions } from '../MessageActions';
5
- import { useMessageContext } from '../../context/MessageContext';
6
- import { useTranslationContext } from '../../context';
6
+ import { useDialogIsOpen } from '../Dialog';
7
+ import { ReactionSelectorWithButton } from '../Reactions/ReactionSelectorWithButton';
8
+ import { useMessageContext, useTranslationContext } from '../../context';
7
9
  const UnMemoizedMessageOptions = (props) => {
8
- const { ActionsIcon = DefaultActionsIcon, displayReplies = true, handleOpenThread: propHandleOpenThread, messageWrapperRef, ReactionIcon = DefaultReactionIcon, theme = 'simple', ThreadIcon = DefaultThreadIcon, } = props;
9
- const { getMessageActions, handleOpenThread: contextHandleOpenThread, initialMessage, message, onReactionListClick, showDetailedReactions, threadList, } = useMessageContext('MessageOptions');
10
+ const { ActionsIcon = DefaultActionsIcon, displayReplies = true, handleOpenThread: propHandleOpenThread, ReactionIcon = DefaultReactionIcon, theme = 'simple', ThreadIcon = DefaultThreadIcon, } = props;
11
+ const { getMessageActions, handleOpenThread: contextHandleOpenThread, initialMessage, message, threadList, } = useMessageContext('MessageOptions');
10
12
  const { t } = useTranslationContext('MessageOptions');
13
+ const messageActionsDialogIsOpen = useDialogIsOpen(`message-actions--${message.id}`);
14
+ const reactionSelectorDialogIsOpen = useDialogIsOpen(`reaction-selector--${message.id}`);
11
15
  const handleOpenThread = propHandleOpenThread || contextHandleOpenThread;
12
16
  const messageActions = getMessageActions();
13
17
  const shouldShowReactions = messageActions.indexOf(MESSAGE_ACTIONS.react) > -1;
@@ -21,12 +25,12 @@ const UnMemoizedMessageOptions = (props) => {
21
25
  initialMessage) {
22
26
  return null;
23
27
  }
24
- const rootClassName = `str-chat__message-${theme}__actions str-chat__message-options`;
25
- return (React.createElement("div", { className: rootClassName, "data-testid": 'message-options' },
26
- React.createElement(MessageActions, { ActionsIcon: ActionsIcon, messageWrapperRef: messageWrapperRef }),
28
+ return (React.createElement("div", { className: clsx(`str-chat__message-${theme}__actions str-chat__message-options`, {
29
+ 'str-chat__message-options--active': messageActionsDialogIsOpen || reactionSelectorDialogIsOpen,
30
+ }), "data-testid": 'message-options' },
31
+ React.createElement(MessageActions, { ActionsIcon: ActionsIcon }),
27
32
  shouldShowReplies && (React.createElement("button", { "aria-label": t('aria/Open Thread'), className: `str-chat__message-${theme}__actions__action str-chat__message-${theme}__actions__action--thread str-chat__message-reply-in-thread-button`, "data-testid": 'thread-action', onClick: handleOpenThread },
28
33
  React.createElement(ThreadIcon, { className: 'str-chat__message-action-icon' }))),
29
- shouldShowReactions && (React.createElement("button", { "aria-expanded": showDetailedReactions, "aria-label": t('aria/Open Reaction Selector'), className: `str-chat__message-${theme}__actions__action str-chat__message-${theme}__actions__action--reactions str-chat__message-reactions-button`, "data-testid": 'message-reaction-action', onClick: onReactionListClick },
30
- React.createElement(ReactionIcon, { className: 'str-chat__message-action-icon' })))));
34
+ shouldShowReactions && (React.createElement(ReactionSelectorWithButton, { ReactionIcon: ReactionIcon, theme: theme }))));
31
35
  };
32
36
  export const MessageOptions = React.memo(UnMemoizedMessageOptions);
@@ -15,18 +15,18 @@ import { CUSTOM_MESSAGE_TYPE } from '../../constants/messageTypes';
15
15
  import { EditMessageForm as DefaultEditMessageForm, MessageInput } from '../MessageInput';
16
16
  import { MML } from '../MML';
17
17
  import { Modal } from '../Modal';
18
- import { ReactionsList as DefaultReactionList, ReactionSelector as DefaultReactionSelector, } from '../Reactions';
18
+ import { ReactionsList as DefaultReactionList } from '../Reactions';
19
19
  import { MessageBounceModal } from '../MessageBounce/MessageBounceModal';
20
20
  import { useComponentContext } from '../../context/ComponentContext';
21
21
  import { useMessageContext } from '../../context/MessageContext';
22
22
  import { useTranslationContext } from '../../context';
23
23
  import { MessageEditedTimestamp } from './MessageEditedTimestamp';
24
24
  const MessageSimpleWithContext = (props) => {
25
- const { additionalMessageInputProps, clearEditingState, editing, endOfGroup, firstOfGroup, groupedByUser, handleAction, handleOpenThread, handleRetry, highlighted, isMyMessage, isReactionEnabled, message, onUserClick, onUserHover, reactionSelectorRef, renderText, showDetailedReactions, threadList, } = props;
25
+ const { additionalMessageInputProps, clearEditingState, editing, endOfGroup, firstOfGroup, groupedByUser, handleAction, handleOpenThread, handleRetry, highlighted, isMyMessage, message, onUserClick, onUserHover, renderText, threadList, } = props;
26
26
  const { t } = useTranslationContext('MessageSimple');
27
27
  const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false);
28
28
  const [isEditedTimestampOpen, setEditedTimestampOpen] = useState(false);
29
- const { Attachment = DefaultAttachment, Avatar = DefaultAvatar, EditMessageInput = DefaultEditMessageForm, MessageDeleted = DefaultMessageDeleted, MessageBouncePrompt = DefaultMessageBouncePrompt, MessageOptions = DefaultMessageOptions, MessageRepliesCountButton = DefaultMessageRepliesCountButton, MessageStatus = DefaultMessageStatus, MessageTimestamp = DefaultMessageTimestamp, ReactionSelector = DefaultReactionSelector, ReactionsList = DefaultReactionList, PinIndicator, } = useComponentContext('MessageSimple');
29
+ const { Attachment = DefaultAttachment, Avatar = DefaultAvatar, EditMessageInput = DefaultEditMessageForm, MessageDeleted = DefaultMessageDeleted, MessageBouncePrompt = DefaultMessageBouncePrompt, MessageOptions = DefaultMessageOptions, MessageRepliesCountButton = DefaultMessageRepliesCountButton, MessageStatus = DefaultMessageStatus, MessageTimestamp = DefaultMessageTimestamp, ReactionsList = DefaultReactionList, PinIndicator, } = useComponentContext('MessageSimple');
30
30
  const hasAttachment = messageHasAttachments(message);
31
31
  const hasReactions = messageHasReactions(message);
32
32
  if (message.customType === CUSTOM_MESSAGE_TYPE.date) {
@@ -35,13 +35,6 @@ const MessageSimpleWithContext = (props) => {
35
35
  if (message.deleted_at || message.type === 'deleted') {
36
36
  return React.createElement(MessageDeleted, { message: message });
37
37
  }
38
- /** FIXME: isReactionEnabled should be removed with next major version and a proper centralized permissions logic should be put in place
39
- * With the current permissions implementation it would be sth like:
40
- * const messageActions = getMessageActions();
41
- * const canReact = messageActions.includes(MESSAGE_ACTIONS.react);
42
- */
43
- const canReact = isReactionEnabled;
44
- const canShowReactions = hasReactions;
45
38
  const showMetadata = !groupedByUser || endOfGroup;
46
39
  const showReplyCountButton = !threadList && !!message.reply_count;
47
40
  const allowRetry = message.status === 'failed' && message.errorStatusCode !== 403;
@@ -63,7 +56,7 @@ const MessageSimpleWithContext = (props) => {
63
56
  'str-chat__message--has-attachment': hasAttachment,
64
57
  'str-chat__message--highlighted': highlighted,
65
58
  'str-chat__message--pinned pinned-message': message.pinned,
66
- 'str-chat__message--with-reactions': canShowReactions,
59
+ 'str-chat__message--with-reactions': hasReactions,
67
60
  'str-chat__message-send-can-be-retried': message?.status === 'failed' && message?.errorStatusCode !== 403,
68
61
  'str-chat__message-with-thread-link': showReplyCountButton,
69
62
  'str-chat__virtual-message__wrapper--end': endOfGroup,
@@ -81,9 +74,7 @@ const MessageSimpleWithContext = (props) => {
81
74
  'str-chat__simple-message--error-failed': allowRetry || isBounced,
82
75
  }), "data-testid": 'message-inner', onClick: handleClick, onKeyUp: handleClick },
83
76
  React.createElement(MessageOptions, null),
84
- React.createElement("div", { className: 'str-chat__message-reactions-host' },
85
- canShowReactions && React.createElement(ReactionsList, { reverse: true }),
86
- showDetailedReactions && canReact && React.createElement(ReactionSelector, { ref: reactionSelectorRef })),
77
+ React.createElement("div", { className: 'str-chat__message-reactions-host' }, hasReactions && React.createElement(ReactionsList, { reverse: true })),
87
78
  React.createElement("div", { className: 'str-chat__message-bubble' },
88
79
  message.attachments?.length && !message.quoted_message ? (React.createElement(Attachment, { actionHandler: handleAction, attachments: message.attachments })) : null,
89
80
  React.createElement(MessageText, { message: message, renderText: renderText }),
@@ -1,11 +1,5 @@
1
- import React, { RefObject } from 'react';
1
+ import React from 'react';
2
2
  import { StreamMessage } from '../../../context/ChannelStateContext';
3
- import type { ReactEventHandler } from '../types';
4
3
  import type { DefaultStreamChatGenerics } from '../../../types/types';
5
4
  export declare const reactionHandlerWarning = "Reaction handler was called, but it is missing one of its required arguments.\nMake sure the ChannelAction and ChannelState contexts are properly set and the hook is initialized with a valid message.";
6
5
  export declare const useReactionHandler: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(message?: StreamMessage<StreamChatGenerics>) => (reactionType: string, event?: React.BaseSyntheticEvent) => Promise<void>;
7
- export declare const useReactionClick: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(message?: StreamMessage<StreamChatGenerics>, reactionSelectorRef?: RefObject<HTMLDivElement | null>, messageWrapperRef?: RefObject<HTMLDivElement | null>, closeReactionSelectorOnClick?: boolean) => {
8
- isReactionEnabled: boolean;
9
- onReactionListClick: ReactEventHandler;
10
- showDetailedReactions: boolean;
11
- };