stream-chat-react 12.0.0-rc.14 → 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.
- package/dist/components/Channel/hooks/useCreateChannelStateContext.js +1 -0
- package/dist/components/ChannelHeader/ChannelHeader.js +4 -5
- package/dist/components/ChannelPreview/hooks/useChannelPreviewInfo.js +14 -16
- package/dist/components/ChannelPreview/utils.js +9 -20
- package/dist/components/ChannelSearch/hooks/useChannelSearch.js +2 -3
- package/dist/components/ChatView/ChatView.js +2 -1
- package/dist/components/Dialog/DialogAnchor.d.ts +25 -0
- package/dist/components/Dialog/DialogAnchor.js +68 -0
- package/dist/components/Dialog/DialogManager.d.ts +43 -0
- package/dist/components/Dialog/DialogManager.js +98 -0
- package/dist/components/Dialog/DialogPortal.d.ts +7 -0
- package/dist/components/Dialog/DialogPortal.js +25 -0
- package/dist/components/Dialog/hooks/index.d.ts +1 -0
- package/dist/components/Dialog/hooks/index.js +1 -0
- package/dist/components/Dialog/hooks/useDialog.d.ts +4 -0
- package/dist/components/Dialog/hooks/useDialog.js +26 -0
- package/dist/components/Dialog/index.d.ts +4 -0
- package/dist/components/Dialog/index.js +4 -0
- package/dist/components/Message/Message.js +3 -5
- package/dist/components/Message/MessageOptions.d.ts +1 -2
- package/dist/components/Message/MessageOptions.js +13 -9
- package/dist/components/Message/MessageSimple.js +5 -14
- package/dist/components/Message/hooks/useReactionHandler.d.ts +1 -7
- package/dist/components/Message/hooks/useReactionHandler.js +1 -63
- package/dist/components/Message/utils.js +3 -0
- package/dist/components/MessageActions/MessageActions.d.ts +1 -2
- package/dist/components/MessageActions/MessageActions.js +13 -47
- package/dist/components/MessageActions/MessageActionsBox.d.ts +1 -1
- package/dist/components/MessageActions/MessageActionsBox.js +6 -6
- package/dist/components/MessageInput/hooks/useUserTrigger.js +0 -1
- package/dist/components/MessageList/MessageList.js +7 -5
- package/dist/components/MessageList/VirtualizedMessageList.js +39 -37
- package/dist/components/Reactions/ReactionSelector.d.ts +1 -1
- package/dist/components/Reactions/ReactionSelector.js +33 -24
- package/dist/components/Reactions/ReactionSelectorWithButton.d.ts +13 -0
- package/dist/components/Reactions/ReactionSelectorWithButton.js +22 -0
- package/dist/components/Reactions/ReactionsList.d.ts +0 -3
- package/dist/components/Thread/Thread.js +2 -1
- package/dist/components/Threads/ThreadList/ThreadList.js +1 -1
- package/dist/components/Threads/ThreadList/ThreadListItemUI.js +1 -1
- package/dist/components/Threads/ThreadList/ThreadListLoadingIndicator.js +1 -1
- package/dist/components/Threads/ThreadList/ThreadListUnseenThreadsBanner.js +1 -1
- package/dist/components/Threads/hooks/useThreadManagerState.js +1 -1
- package/dist/components/Threads/hooks/useThreadState.js +1 -1
- package/dist/components/Threads/index.d.ts +0 -1
- package/dist/components/Threads/index.js +0 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/context/DialogManagerContext.d.ts +10 -0
- package/dist/context/DialogManagerContext.js +14 -0
- package/dist/context/MessageContext.d.ts +2 -10
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +1 -0
- package/dist/css/v2/index.css +1 -1
- package/dist/css/v2/index.layout.css +1 -1
- package/dist/index.browser.cjs +2164 -2004
- package/dist/index.browser.cjs.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.node.cjs +2087 -1918
- package/dist/index.node.cjs.map +4 -4
- package/dist/scss/v2/Dialog/Dialog-layout.scss +8 -0
- package/dist/scss/v2/Message/Message-layout.scss +8 -0
- package/dist/scss/v2/MessageReactions/MessageReactionsSelector-layout.scss +16 -0
- package/dist/scss/v2/index.layout.scss +1 -0
- package/dist/store/hooks/index.d.ts +1 -0
- package/dist/store/hooks/index.js +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.js +1 -0
- package/package.json +2 -2
- /package/dist/{components/Threads → store}/hooks/useStateStore.d.ts +0 -0
- /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
|
-
|
|
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('
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
18
|
+
updateTitles();
|
|
19
|
+
client.on('user.updated', updateTitles);
|
|
21
20
|
return () => {
|
|
22
|
-
client.off('user.updated',
|
|
21
|
+
client.off('user.updated', updateTitles);
|
|
23
22
|
};
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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 (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useCallback, useMemo
|
|
2
|
-
import { useActionHandler, useDeleteHandler, useEditHandler, useFlagHandler, useMarkUnreadHandler, useMentionsHandler, useMuteHandler, useOpenThreadHandler, usePinHandler,
|
|
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,
|
|
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 {
|
|
6
|
-
import {
|
|
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,
|
|
9
|
-
const { getMessageActions, handleOpenThread: contextHandleOpenThread, initialMessage, message,
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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(
|
|
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
|
|
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,
|
|
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,
|
|
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':
|
|
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
|
|
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
|
-
};
|