stream-chat-react 13.8.1 → 13.10.0
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/Channel.d.ts +2 -2
- package/dist/components/Channel/Channel.js +3 -3
- package/dist/components/Channel/hooks/useCreateChannelStateContext.js +3 -1
- package/dist/components/ChannelPreview/hooks/useMessageDeliveryStatus.d.ts +1 -0
- package/dist/components/ChannelPreview/hooks/useMessageDeliveryStatus.js +31 -14
- package/dist/components/Chat/hooks/useChat.js +1 -1
- package/dist/components/Dialog/ButtonWithSubmenu.d.ts +2 -2
- package/dist/components/Dialog/ButtonWithSubmenu.js +6 -6
- package/dist/components/Dialog/DialogAnchor.d.ts +5 -11
- package/dist/components/Dialog/DialogAnchor.js +22 -26
- package/dist/components/Dialog/DialogPortal.d.ts +2 -1
- package/dist/components/Dialog/DialogPortal.js +23 -7
- package/dist/components/Dialog/hooks/index.d.ts +1 -0
- package/dist/components/Dialog/hooks/useDialog.d.ts +4 -0
- package/dist/components/Dialog/hooks/useDialog.js +9 -1
- package/dist/components/Dialog/hooks/usePopoverPosition.d.ts +68 -0
- package/dist/components/Dialog/hooks/usePopoverPosition.js +54 -0
- package/dist/components/Form/Dropdown.d.ts +2 -2
- package/dist/components/Message/Message.js +1 -1
- package/dist/components/Message/MessageRepliesCountButton.js +3 -1
- package/dist/components/Message/MessageStatus.d.ts +1 -0
- package/dist/components/Message/MessageStatus.js +22 -12
- package/dist/components/Message/hooks/useDeleteHandler.js +2 -2
- package/dist/components/Message/icons.d.ts +1 -0
- package/dist/components/Message/icons.js +6 -2
- package/dist/components/Message/renderText/remarkPlugins/index.d.ts +1 -0
- package/dist/components/Message/renderText/remarkPlugins/index.js +1 -0
- package/dist/components/Message/renderText/remarkPlugins/keepLineBreaksPlugin.d.ts +10 -2
- package/dist/components/Message/renderText/remarkPlugins/keepLineBreaksPlugin.js +46 -26
- package/dist/components/Message/renderText/remarkPlugins/remarkIgnoreMarkdown.d.ts +8 -0
- package/dist/components/Message/renderText/remarkPlugins/remarkIgnoreMarkdown.js +11 -0
- package/dist/components/Message/types.d.ts +2 -0
- package/dist/components/Message/utils.d.ts +4 -1
- package/dist/components/Message/utils.js +9 -0
- package/dist/components/MessageActions/MessageActions.js +4 -4
- package/dist/components/MessageActions/MessageActionsBox.js +3 -1
- package/dist/components/MessageInput/AttachmentSelector.d.ts +1 -1
- package/dist/components/MessageInput/AttachmentSelector.js +9 -4
- package/dist/components/MessageList/MessageList.js +2 -2
- package/dist/components/MessageList/VirtualizedMessageList.d.ts +2 -0
- package/dist/components/MessageList/VirtualizedMessageList.js +10 -4
- package/dist/components/MessageList/VirtualizedMessageListComponents.js +2 -2
- package/dist/components/MessageList/hooks/MessageList/useMessageListElements.d.ts +2 -2
- package/dist/components/MessageList/hooks/MessageList/useMessageListElements.js +13 -5
- package/dist/components/MessageList/hooks/useLastDeliveredData.d.ts +8 -0
- package/dist/components/MessageList/hooks/useLastDeliveredData.js +16 -0
- package/dist/components/MessageList/hooks/useLastReadData.d.ts +3 -8
- package/dist/components/MessageList/hooks/useLastReadData.js +10 -7
- package/dist/components/MessageList/renderMessages.d.ts +5 -4
- package/dist/components/MessageList/renderMessages.js +2 -2
- package/dist/components/MessageList/utils.d.ts +1 -5
- package/dist/components/MessageList/utils.js +0 -30
- package/dist/components/Modal/GlobalModal.js +2 -2
- package/dist/components/Reactions/ReactionSelectorWithButton.js +4 -4
- package/dist/components/Tooltip/Tooltip.d.ts +2 -2
- package/dist/components/Tooltip/Tooltip.js +11 -12
- package/dist/context/ChannelActionContext.d.ts +2 -2
- package/dist/context/DialogManagerContext.d.ts +1 -0
- package/dist/context/DialogManagerContext.js +1 -0
- package/dist/context/MessageContext.d.ts +5 -3
- package/dist/css/v2/index.css +1 -1
- package/dist/css/v2/index.layout.css +1 -1
- package/dist/experimental/MessageActions/MessageActions.js +5 -5
- package/dist/experimental/index.browser.cjs +324 -249
- package/dist/experimental/index.browser.cjs.map +4 -4
- package/dist/experimental/index.node.cjs +324 -249
- package/dist/experimental/index.node.cjs.map +4 -4
- package/dist/i18n/Streami18n.d.ts +2 -0
- package/dist/i18n/de.json +2 -0
- package/dist/i18n/en.json +2 -0
- package/dist/i18n/es.json +2 -0
- package/dist/i18n/fr.json +2 -0
- package/dist/i18n/hi.json +2 -0
- package/dist/i18n/it.json +2 -0
- package/dist/i18n/ja.json +2 -0
- package/dist/i18n/ko.json +2 -0
- package/dist/i18n/nl.json +2 -0
- package/dist/i18n/pt.json +2 -0
- package/dist/i18n/ru.json +2 -0
- package/dist/i18n/tr.json +2 -0
- package/dist/index.browser.cjs +1587 -1380
- package/dist/index.browser.cjs.map +4 -4
- package/dist/index.node.cjs +1592 -1381
- package/dist/index.node.cjs.map +4 -4
- package/dist/plugins/Emojis/EmojiPicker.d.ts +9 -4
- package/dist/plugins/Emojis/EmojiPicker.js +10 -5
- package/dist/plugins/Emojis/index.browser.cjs +89 -29
- package/dist/plugins/Emojis/index.browser.cjs.map +4 -4
- package/dist/plugins/Emojis/index.node.cjs +89 -29
- package/dist/plugins/Emojis/index.node.cjs.map +4 -4
- package/dist/scss/v2/Dialog/Dialog-layout.scss +12 -3
- package/dist/scss/v2/Icon/Icon-layout.scss +6 -0
- package/dist/scss/v2/Icon/Icon-theme.scss +4 -0
- package/dist/scss/v2/Message/Message-layout.scss +34 -3
- package/dist/scss/v2/Message/Message-theme.scss +9 -0
- package/dist/scss/v2/MessageActionsBox/MessageActionsBox-layout.scss +1 -0
- package/dist/scss/v2/MessageInput/MessageInput-layout.scss +8 -0
- package/dist/scss/v2/MessageInput/MessageInput-theme.scss +2 -1
- package/dist/scss/v2/Poll/Poll-layout.scss +29 -7
- package/package.json +7 -8
- package/dist/components/MessageActions/hooks/index.d.ts +0 -1
- package/dist/components/MessageActions/hooks/index.js +0 -1
- package/dist/components/MessageActions/hooks/useMessageActionsBoxPopper.d.ts +0 -18
- package/dist/components/MessageActions/hooks/useMessageActionsBoxPopper.js +0 -31
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PropsWithChildren } from 'react';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import type { ChannelQueryOptions, EventAPIResponse, LocalMessage, Message, MessageResponse, SendMessageOptions, Channel as StreamChannel, StreamChat, UpdateMessageOptions } from 'stream-chat';
|
|
3
|
+
import type { ChannelQueryOptions, DeleteMessageOptions, EventAPIResponse, LocalMessage, Message, MessageResponse, SendMessageOptions, Channel as StreamChannel, StreamChat, UpdateMessageOptions } from 'stream-chat';
|
|
4
4
|
import type { OnMentionAction } from './hooks/useMentionsHandlers';
|
|
5
5
|
import type { LoadingErrorIndicatorProps } from '../Loading';
|
|
6
6
|
import type { ComponentContextValue } from '../../context';
|
|
@@ -19,7 +19,7 @@ export type ChannelProps = ChannelPropsForwardedToComponentContext & {
|
|
|
19
19
|
*/
|
|
20
20
|
channelQueryOptions?: ChannelQueryOptions;
|
|
21
21
|
/** Custom action handler to override the default `client.deleteMessage(message.id)` function */
|
|
22
|
-
doDeleteMessageRequest?: (message: LocalMessage) => Promise<MessageResponse>;
|
|
22
|
+
doDeleteMessageRequest?: (message: LocalMessage, options?: DeleteMessageOptions) => Promise<MessageResponse>;
|
|
23
23
|
/** Custom action handler to override the default `channel.markRead` request function (advanced usage only) */
|
|
24
24
|
doMarkReadRequest?: (channel: StreamChannel, setChannelUnreadUiState?: (state: ChannelUnreadUiState) => void) => Promise<EventAPIResponse> | void;
|
|
25
25
|
/** Custom action handler to override the default `channel.sendMessage` request function (advanced usage only) */
|
|
@@ -500,16 +500,16 @@ const ChannelInner = (props) => {
|
|
|
500
500
|
t,
|
|
501
501
|
channelUnreadUiState,
|
|
502
502
|
]);
|
|
503
|
-
const deleteMessage = useCallback(async (message) => {
|
|
503
|
+
const deleteMessage = useCallback(async (message, options) => {
|
|
504
504
|
if (!message?.id) {
|
|
505
505
|
throw new Error('Cannot delete a message - missing message ID.');
|
|
506
506
|
}
|
|
507
507
|
let deletedMessage;
|
|
508
508
|
if (doDeleteMessageRequest) {
|
|
509
|
-
deletedMessage = await doDeleteMessageRequest(message);
|
|
509
|
+
deletedMessage = await doDeleteMessageRequest(message, options);
|
|
510
510
|
}
|
|
511
511
|
else {
|
|
512
|
-
const result = await client.deleteMessage(message.id);
|
|
512
|
+
const result = await client.deleteMessage(message.id, options);
|
|
513
513
|
deletedMessage = result.message;
|
|
514
514
|
}
|
|
515
515
|
return deletedMessage;
|
|
@@ -16,10 +16,12 @@ export const useCreateChannelStateContext = (value) => {
|
|
|
16
16
|
channelCapabilitiesArray.forEach((capability) => {
|
|
17
17
|
channelCapabilities[capability] = true;
|
|
18
18
|
});
|
|
19
|
+
// FIXME: this is crazy - I could not find out why the messages were not getting updated when only message properties that are not part
|
|
20
|
+
// of this serialization has been changed. A great example of memoization gone wrong.
|
|
19
21
|
const memoizedMessageData = skipMessageDataMemoization
|
|
20
22
|
? messages
|
|
21
23
|
: messages
|
|
22
|
-
.map(({ deleted_at, latest_reactions, pinned, reply_count, status, updated_at, user, }) => `${deleted_at}${latest_reactions ? latest_reactions.map(({ type }) => type).join() : ''}${pinned}${reply_count}${status}${updated_at && (isDayOrMoment(updated_at) || isDate(updated_at))
|
|
24
|
+
.map(({ deleted_at, latest_reactions, pinned, reply_count, status, type, updated_at, user, }) => `${type}${deleted_at}${latest_reactions ? latest_reactions.map(({ type }) => type).join() : ''}${pinned}${reply_count}${status}${updated_at && (isDayOrMoment(updated_at) || isDate(updated_at))
|
|
23
25
|
? updated_at.toISOString()
|
|
24
26
|
: updated_at || ''}${user?.updated_at}`)
|
|
25
27
|
.join();
|
|
@@ -2,53 +2,70 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { useChatContext } from '../../../context';
|
|
3
3
|
export var MessageDeliveryStatus;
|
|
4
4
|
(function (MessageDeliveryStatus) {
|
|
5
|
+
MessageDeliveryStatus["SENT"] = "sent";
|
|
5
6
|
MessageDeliveryStatus["DELIVERED"] = "delivered";
|
|
6
7
|
MessageDeliveryStatus["READ"] = "read";
|
|
7
8
|
})(MessageDeliveryStatus || (MessageDeliveryStatus = {}));
|
|
8
9
|
export const useMessageDeliveryStatus = ({ channel, lastMessage, }) => {
|
|
9
10
|
const { client } = useChatContext();
|
|
10
11
|
const [messageDeliveryStatus, setMessageDeliveryStatus] = useState();
|
|
11
|
-
const isOwnMessage = useCallback((message) => client.user && message
|
|
12
|
+
const isOwnMessage = useCallback((message) => client.user && message && message.user?.id === client.user.id, [client]);
|
|
12
13
|
useEffect(() => {
|
|
14
|
+
// empty channel
|
|
15
|
+
if (!lastMessage) {
|
|
16
|
+
setMessageDeliveryStatus(undefined);
|
|
17
|
+
}
|
|
13
18
|
const lastMessageIsOwn = isOwnMessage(lastMessage);
|
|
14
19
|
if (!lastMessage?.created_at || !lastMessageIsOwn)
|
|
15
20
|
return;
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
: lastMessage.created_at
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const msgRef = {
|
|
22
|
+
msgId: lastMessage.id,
|
|
23
|
+
timestampMs: lastMessage.created_at.getTime(),
|
|
24
|
+
};
|
|
25
|
+
const readersForMessage = channel.messageReceiptsTracker.readersForMessage(msgRef);
|
|
26
|
+
const deliveredForMessage = channel.messageReceiptsTracker.deliveredForMessage(msgRef);
|
|
27
|
+
setMessageDeliveryStatus(readersForMessage.length > 1 ||
|
|
28
|
+
(readersForMessage.length === 1 && readersForMessage[0].id !== client.user?.id)
|
|
24
29
|
? MessageDeliveryStatus.READ
|
|
25
|
-
:
|
|
26
|
-
|
|
30
|
+
: deliveredForMessage.length > 1 ||
|
|
31
|
+
(deliveredForMessage.length === 1 &&
|
|
32
|
+
deliveredForMessage[0].id !== client.user?.id)
|
|
33
|
+
? MessageDeliveryStatus.DELIVERED
|
|
34
|
+
: MessageDeliveryStatus.SENT);
|
|
35
|
+
}, [channel, client, isOwnMessage, lastMessage]);
|
|
27
36
|
useEffect(() => {
|
|
28
37
|
const handleMessageNew = (event) => {
|
|
29
38
|
// the last message is not mine, so do not show the delivery status
|
|
30
39
|
if (!isOwnMessage(event.message)) {
|
|
31
40
|
return setMessageDeliveryStatus(undefined);
|
|
32
41
|
}
|
|
33
|
-
return setMessageDeliveryStatus(MessageDeliveryStatus.
|
|
42
|
+
return setMessageDeliveryStatus(MessageDeliveryStatus.SENT);
|
|
34
43
|
};
|
|
35
44
|
channel.on('message.new', handleMessageNew);
|
|
36
45
|
return () => {
|
|
37
46
|
channel.off('message.new', handleMessageNew);
|
|
38
47
|
};
|
|
39
|
-
}, [channel,
|
|
48
|
+
}, [channel, isOwnMessage]);
|
|
40
49
|
useEffect(() => {
|
|
41
50
|
if (!isOwnMessage(lastMessage))
|
|
42
51
|
return;
|
|
52
|
+
const handleMessageDelivered = (event) => {
|
|
53
|
+
if (event.user?.id !== client.user?.id &&
|
|
54
|
+
lastMessage &&
|
|
55
|
+
lastMessage.id === event.last_delivered_message_id)
|
|
56
|
+
setMessageDeliveryStatus(MessageDeliveryStatus.DELIVERED);
|
|
57
|
+
};
|
|
43
58
|
const handleMarkRead = (event) => {
|
|
44
59
|
if (event.user?.id !== client.user?.id)
|
|
45
60
|
setMessageDeliveryStatus(MessageDeliveryStatus.READ);
|
|
46
61
|
};
|
|
62
|
+
channel.on('message.delivered', handleMessageDelivered);
|
|
47
63
|
channel.on('message.read', handleMarkRead);
|
|
48
64
|
return () => {
|
|
65
|
+
channel.off('message.delivered', handleMessageDelivered);
|
|
49
66
|
channel.off('message.read', handleMarkRead);
|
|
50
67
|
};
|
|
51
|
-
}, [channel, client,
|
|
68
|
+
}, [channel, client, isOwnMessage, lastMessage]);
|
|
52
69
|
return {
|
|
53
70
|
messageDeliveryStatus,
|
|
54
71
|
};
|
|
@@ -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.
|
|
27
|
+
const version = "13.10.0";
|
|
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,9 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { ComponentProps, ComponentType } from 'react';
|
|
3
|
-
import type {
|
|
3
|
+
import type { PopperLikePlacement } from './hooks';
|
|
4
4
|
type ButtonWithSubmenu = ComponentProps<'button'> & {
|
|
5
5
|
children: React.ReactNode;
|
|
6
|
-
placement:
|
|
6
|
+
placement: PopperLikePlacement;
|
|
7
7
|
Submenu: ComponentType;
|
|
8
8
|
submenuContainerProps?: ComponentProps<'div'>;
|
|
9
9
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { useDialogIsOpen, useDialogOnNearestManager } from './hooks';
|
|
4
4
|
import { useDialogAnchor } from './DialogAnchor';
|
|
5
5
|
export const ButtonWithSubmenu = ({ children, className, placement, Submenu, submenuContainerProps, ...buttonProps }) => {
|
|
6
6
|
const buttonRef = useRef(null);
|
|
@@ -8,9 +8,9 @@ export const ButtonWithSubmenu = ({ children, className, placement, Submenu, sub
|
|
|
8
8
|
const keepSubmenuOpen = useRef(false);
|
|
9
9
|
const dialogCloseTimeout = useRef(null);
|
|
10
10
|
const dialogId = useMemo(() => `submenu-${Math.random().toString(36).slice(2)}`, []);
|
|
11
|
-
const dialog =
|
|
12
|
-
const dialogIsOpen = useDialogIsOpen(dialogId);
|
|
13
|
-
const {
|
|
11
|
+
const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId });
|
|
12
|
+
const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id);
|
|
13
|
+
const { setPopperElement, styles } = useDialogAnchor({
|
|
14
14
|
open: dialogIsOpen,
|
|
15
15
|
placement,
|
|
16
16
|
referenceElement: buttonRef.current,
|
|
@@ -66,7 +66,7 @@ export const ButtonWithSubmenu = ({ children, className, placement, Submenu, sub
|
|
|
66
66
|
keepSubmenuOpen.current = false;
|
|
67
67
|
closeDialogLazily();
|
|
68
68
|
}, ref: buttonRef, role: 'option', ...buttonProps }, children),
|
|
69
|
-
dialogIsOpen && (React.createElement("div", {
|
|
69
|
+
dialogIsOpen && (React.createElement("div", { onBlur: (event) => {
|
|
70
70
|
const isBlurredDescendant = event.relatedTarget instanceof Node &&
|
|
71
71
|
dialogContainer?.contains(event.relatedTarget);
|
|
72
72
|
if (isBlurredDescendant)
|
|
@@ -83,6 +83,6 @@ export const ButtonWithSubmenu = ({ children, className, placement, Submenu, sub
|
|
|
83
83
|
}, ref: (element) => {
|
|
84
84
|
setPopperElement(element);
|
|
85
85
|
setDialogContainer(element);
|
|
86
|
-
}, style: styles
|
|
86
|
+
}, style: styles, tabIndex: -1, ...submenuContainerProps },
|
|
87
87
|
React.createElement(Submenu, null)))));
|
|
88
88
|
};
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import type { Placement } from '@popperjs/core';
|
|
2
1
|
import type { ComponentProps, PropsWithChildren } from 'react';
|
|
3
2
|
import React from 'react';
|
|
3
|
+
import type { PopperLikePlacement } from './hooks';
|
|
4
4
|
export interface DialogAnchorOptions {
|
|
5
5
|
open: boolean;
|
|
6
|
-
placement:
|
|
6
|
+
placement: PopperLikePlacement;
|
|
7
7
|
referenceElement: HTMLElement | null;
|
|
8
8
|
allowFlip?: boolean;
|
|
9
9
|
}
|
|
10
10
|
export declare function useDialogAnchor<T extends HTMLElement>({ allowFlip, open, placement, referenceElement, }: DialogAnchorOptions): {
|
|
11
|
-
attributes: {
|
|
12
|
-
[key: string]: {
|
|
13
|
-
[key: string]: string;
|
|
14
|
-
} | undefined;
|
|
15
|
-
};
|
|
16
11
|
setPopperElement: React.Dispatch<React.SetStateAction<T | null>>;
|
|
17
|
-
styles:
|
|
18
|
-
[key: string]: React.CSSProperties;
|
|
19
|
-
};
|
|
12
|
+
styles: React.CSSProperties;
|
|
20
13
|
};
|
|
21
14
|
export type DialogAnchorProps = PropsWithChildren<Partial<DialogAnchorOptions>> & {
|
|
22
15
|
id: string;
|
|
16
|
+
dialogManagerId?: string;
|
|
23
17
|
focus?: boolean;
|
|
24
18
|
trapFocus?: boolean;
|
|
25
19
|
} & ComponentProps<'div'>;
|
|
26
|
-
export declare const DialogAnchor: ({ allowFlip, children, className, focus, id, placement, referenceElement, tabIndex, trapFocus, ...restDivProps }: DialogAnchorProps) => React.JSX.Element | null;
|
|
20
|
+
export declare const DialogAnchor: ({ allowFlip, children, className, dialogManagerId, focus, id, placement, referenceElement, tabIndex, trapFocus, ...restDivProps }: DialogAnchorProps) => React.JSX.Element | null;
|
|
@@ -1,29 +1,22 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import React, { useEffect, useState } from 'react';
|
|
3
3
|
import { FocusScope } from '@react-aria/focus';
|
|
4
|
-
import { usePopper } from 'react-popper';
|
|
5
4
|
import { DialogPortalEntry } from './DialogPortal';
|
|
6
5
|
import { useDialog, useDialogIsOpen } from './hooks';
|
|
6
|
+
import { usePopoverPosition } from './hooks/usePopoverPosition';
|
|
7
7
|
export function useDialogAnchor({ allowFlip, open, placement, referenceElement, }) {
|
|
8
8
|
const [popperElement, setPopperElement] = useState(null);
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
enabled: !!allowFlip, // Prevent flipping
|
|
13
|
-
name: 'flip',
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
name: 'eventListeners',
|
|
17
|
-
options: {
|
|
18
|
-
// It's not safe to update popper position on resize and scroll, since popper's
|
|
19
|
-
// reference element might not be visible at the time.
|
|
20
|
-
resize: false,
|
|
21
|
-
scroll: false,
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
],
|
|
9
|
+
const { refs, strategy, update, x, y } = usePopoverPosition({
|
|
10
|
+
allowFlip,
|
|
11
|
+
freeze: true,
|
|
25
12
|
placement,
|
|
26
13
|
});
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
refs.setReference(referenceElement);
|
|
16
|
+
}, [referenceElement, refs]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
refs.setFloating(popperElement);
|
|
19
|
+
}, [popperElement, refs]);
|
|
27
20
|
useEffect(() => {
|
|
28
21
|
if (open && popperElement) {
|
|
29
22
|
// Since the popper's reference element might not be (and usually is not) visible
|
|
@@ -31,20 +24,23 @@ export function useDialogAnchor({ allowFlip, open, placement, referenceElement,
|
|
|
31
24
|
// update is non-null only if popperElement is non-null
|
|
32
25
|
update?.();
|
|
33
26
|
}
|
|
34
|
-
}, [open, popperElement, update]);
|
|
27
|
+
}, [open, placement, popperElement, update]);
|
|
35
28
|
if (popperElement && !open) {
|
|
36
29
|
setPopperElement(null);
|
|
37
30
|
}
|
|
38
31
|
return {
|
|
39
|
-
attributes,
|
|
40
32
|
setPopperElement,
|
|
41
|
-
styles
|
|
33
|
+
styles: {
|
|
34
|
+
left: x ?? 0,
|
|
35
|
+
position: strategy,
|
|
36
|
+
top: y ?? 0,
|
|
37
|
+
},
|
|
42
38
|
};
|
|
43
39
|
}
|
|
44
|
-
export const DialogAnchor = ({ allowFlip = true, children, className, focus = true, id, placement = 'auto', referenceElement = null, tabIndex, trapFocus, ...restDivProps }) => {
|
|
45
|
-
const dialog = useDialog({ id });
|
|
46
|
-
const open = useDialogIsOpen(id);
|
|
47
|
-
const {
|
|
40
|
+
export const DialogAnchor = ({ allowFlip = true, children, className, dialogManagerId, focus = true, id, placement = 'auto', referenceElement = null, tabIndex, trapFocus, ...restDivProps }) => {
|
|
41
|
+
const dialog = useDialog({ dialogManagerId, id });
|
|
42
|
+
const open = useDialogIsOpen(id, dialogManagerId);
|
|
43
|
+
const { setPopperElement, styles } = useDialogAnchor({
|
|
48
44
|
allowFlip,
|
|
49
45
|
open,
|
|
50
46
|
placement,
|
|
@@ -67,7 +63,7 @@ export const DialogAnchor = ({ allowFlip = true, children, className, focus = tr
|
|
|
67
63
|
if (!open) {
|
|
68
64
|
return null;
|
|
69
65
|
}
|
|
70
|
-
return (React.createElement(DialogPortalEntry, { dialogId: id },
|
|
66
|
+
return (React.createElement(DialogPortalEntry, { dialogId: id, dialogManagerId: dialogManagerId },
|
|
71
67
|
React.createElement(FocusScope, { autoFocus: focus, contain: trapFocus, restoreFocus: true },
|
|
72
|
-
React.createElement("div", { ...restDivProps,
|
|
68
|
+
React.createElement("div", { ...restDivProps, className: clsx('str-chat__dialog-contents', className), "data-testid": 'str-chat__dialog-contents', ref: setPopperElement, style: styles, tabIndex: typeof tabIndex !== 'undefined' ? tabIndex : 0 }, children))));
|
|
73
69
|
};
|
|
@@ -3,6 +3,7 @@ import React from 'react';
|
|
|
3
3
|
export declare const DialogPortalDestination: () => React.JSX.Element | null;
|
|
4
4
|
type DialogPortalEntryProps = {
|
|
5
5
|
dialogId: string;
|
|
6
|
+
dialogManagerId?: string;
|
|
6
7
|
};
|
|
7
|
-
export declare const DialogPortalEntry: ({ children, dialogId, }: PropsWithChildren<DialogPortalEntryProps>) => React.JSX.Element;
|
|
8
|
+
export declare const DialogPortalEntry: ({ children, dialogId, dialogManagerId, }: PropsWithChildren<DialogPortalEntryProps>) => React.JSX.Element;
|
|
8
9
|
export {};
|
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
import React, { useCallback } from 'react';
|
|
2
2
|
import { useDialogIsOpen, useOpenedDialogCount } from './hooks';
|
|
3
3
|
import { Portal } from '../Portal/Portal';
|
|
4
|
-
import { useDialogManager } from '../../context';
|
|
4
|
+
import { useDialogManager, useNearestDialogManagerContext } from '../../context';
|
|
5
5
|
export const DialogPortalDestination = () => {
|
|
6
|
-
const { dialogManager } =
|
|
7
|
-
const openedDialogCount = useOpenedDialogCount();
|
|
6
|
+
const { dialogManager } = useNearestDialogManagerContext() ?? {};
|
|
7
|
+
const openedDialogCount = useOpenedDialogCount({ dialogManagerId: dialogManager?.id });
|
|
8
|
+
// const [destinationRoot, setDestinationRoot] = useState<HTMLDivElement | null>(null);
|
|
9
|
+
// todo: allow to configure and then enable
|
|
10
|
+
// useEffect(() => {
|
|
11
|
+
// if (!destinationRoot) return;
|
|
12
|
+
// const handleClickOutside = (event: MouseEvent) => {
|
|
13
|
+
// if (!destinationRoot?.contains(event.target as Node)) {
|
|
14
|
+
// dialogManager?.closeAll();
|
|
15
|
+
// }
|
|
16
|
+
// };
|
|
17
|
+
// document.addEventListener('click', handleClickOutside, { capture: true });
|
|
18
|
+
// return () => {
|
|
19
|
+
// document.removeEventListener('click', handleClickOutside, { capture: true });
|
|
20
|
+
// };
|
|
21
|
+
// }, [destinationRoot, dialogManager]);
|
|
8
22
|
if (!openedDialogCount)
|
|
9
23
|
return null;
|
|
10
|
-
return (React.createElement("div", { className: 'str-chat__dialog-overlay', "data-str-chat__portal-id": dialogManager
|
|
24
|
+
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(),
|
|
25
|
+
// ref={setDestinationRoot}
|
|
26
|
+
style: {
|
|
11
27
|
'--str-chat__dialog-overlay-height': openedDialogCount > 0 ? '100%' : '0',
|
|
12
28
|
} }));
|
|
13
29
|
};
|
|
14
|
-
export const DialogPortalEntry = ({ children, dialogId, }) => {
|
|
15
|
-
const { dialogManager } = useDialogManager({ dialogId });
|
|
16
|
-
const dialogIsOpen = useDialogIsOpen(dialogId,
|
|
30
|
+
export const DialogPortalEntry = ({ children, dialogId, dialogManagerId, }) => {
|
|
31
|
+
const { dialogManager } = useDialogManager({ dialogId, dialogManagerId });
|
|
32
|
+
const dialogIsOpen = useDialogIsOpen(dialogId, dialogManagerId);
|
|
17
33
|
const getPortalDestination = useCallback(() => document.querySelector(`div[data-str-chat__portal-id="${dialogManager.id}"]`), [dialogManager.id]);
|
|
18
34
|
return (React.createElement(Portal, { getPortalDestination: getPortalDestination, isOpen: dialogIsOpen }, children));
|
|
19
35
|
};
|
|
@@ -3,6 +3,10 @@ export type UseDialogParams = GetOrCreateDialogParams & {
|
|
|
3
3
|
dialogManagerId?: string;
|
|
4
4
|
};
|
|
5
5
|
export declare const useDialog: ({ dialogManagerId, id }: UseDialogParams) => import("../DialogManager").Dialog;
|
|
6
|
+
export declare const useDialogOnNearestManager: ({ id }: Pick<UseDialogParams, 'id'>) => {
|
|
7
|
+
dialog: import("../DialogManager").Dialog;
|
|
8
|
+
dialogManager: import("../DialogManager").DialogManager | undefined;
|
|
9
|
+
};
|
|
6
10
|
export declare const modalDialogId: "modal-dialog";
|
|
7
11
|
export declare const useModalDialog: () => import("../DialogManager").Dialog;
|
|
8
12
|
export declare const useDialogIsOpen: (id: string, dialogManagerId?: string) => boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect } from 'react';
|
|
2
|
-
import { modalDialogManagerId, useDialogManager } from '../../../context';
|
|
2
|
+
import { modalDialogManagerId, useDialogManager, useNearestDialogManagerContext, } from '../../../context';
|
|
3
3
|
import { useStateStore } from '../../../store';
|
|
4
4
|
export const useDialog = ({ dialogManagerId, id }) => {
|
|
5
5
|
const { dialogManager } = useDialogManager({ dialogManagerId });
|
|
@@ -12,6 +12,14 @@ export const useDialog = ({ dialogManagerId, id }) => {
|
|
|
12
12
|
}, [dialogManager, id]);
|
|
13
13
|
return dialogManager.getOrCreate({ id });
|
|
14
14
|
};
|
|
15
|
+
export const useDialogOnNearestManager = ({ id }) => {
|
|
16
|
+
const { dialogManager } = useNearestDialogManagerContext() ?? {};
|
|
17
|
+
const dialog = useDialog({ dialogManagerId: dialogManager?.id, id });
|
|
18
|
+
return {
|
|
19
|
+
dialog,
|
|
20
|
+
dialogManager,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
15
23
|
export const modalDialogId = 'modal-dialog';
|
|
16
24
|
export const useModalDialog = () => useDialog({ dialogManagerId: modalDialogManagerId, id: modalDialogId });
|
|
17
25
|
export const useDialogIsOpen = (id, dialogManagerId) => {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { autoUpdate, type Placement } from '@floating-ui/react';
|
|
3
|
+
export type PopperLikePlacement = Placement | 'auto' | 'auto-start' | 'auto-end';
|
|
4
|
+
type OffsetOpt = number | {
|
|
5
|
+
mainAxis?: number;
|
|
6
|
+
crossAxis?: number;
|
|
7
|
+
alignmentAxis?: number;
|
|
8
|
+
} | [crossAxis: number, mainAxis: number];
|
|
9
|
+
export type UsePopoverParams = {
|
|
10
|
+
placement?: PopperLikePlacement;
|
|
11
|
+
/** Add flip() when placement is not 'auto*' */
|
|
12
|
+
allowFlip?: boolean;
|
|
13
|
+
/** Keep in viewport; default true to match common popper setups */
|
|
14
|
+
allowShift?: boolean;
|
|
15
|
+
/** The floating UI is fitted to the available space (by constraining its max size) instead of letting it overflow; default false */
|
|
16
|
+
fitAvailableSpace?: boolean;
|
|
17
|
+
/** Offset (number, object, or [crossAxis, mainAxis] tuple) */
|
|
18
|
+
offset?: OffsetOpt;
|
|
19
|
+
/**
|
|
20
|
+
* Freeze behavior like Popper's eventListeners: { scroll:false, resize:false }.
|
|
21
|
+
* If true → no autoUpdate (you can call `update()` manually).
|
|
22
|
+
*/
|
|
23
|
+
freeze?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Fine-grained control of autoUpdate triggers (only if freeze=false).
|
|
26
|
+
* Defaults match Popper's "disabled" example when all set to false.
|
|
27
|
+
*/
|
|
28
|
+
autoUpdateOptions?: Partial<Parameters<typeof autoUpdate>[3]>;
|
|
29
|
+
};
|
|
30
|
+
export declare function usePopoverPosition({ allowFlip, allowShift, autoUpdateOptions, fitAvailableSpace, freeze, offset, placement, }: UsePopoverParams): {
|
|
31
|
+
placement: Placement;
|
|
32
|
+
strategy: import("@floating-ui/utils").Strategy;
|
|
33
|
+
middlewareData: import("@floating-ui/core").MiddlewareData;
|
|
34
|
+
y: number;
|
|
35
|
+
x: number;
|
|
36
|
+
isPositioned: boolean;
|
|
37
|
+
update: () => void;
|
|
38
|
+
floatingStyles: import("react").CSSProperties;
|
|
39
|
+
refs: {
|
|
40
|
+
reference: import("react").MutableRefObject<import("@floating-ui/react-dom").ReferenceType | null>;
|
|
41
|
+
floating: import("react").MutableRefObject<HTMLElement | null>;
|
|
42
|
+
setReference: (node: import("@floating-ui/react-dom").ReferenceType | null) => void;
|
|
43
|
+
setFloating: (node: HTMLElement | null) => void;
|
|
44
|
+
} & import("@floating-ui/react").ExtendedRefs<import("@floating-ui/react").ReferenceType>;
|
|
45
|
+
elements: {
|
|
46
|
+
reference: import("@floating-ui/react-dom").ReferenceType | null;
|
|
47
|
+
floating: HTMLElement | null;
|
|
48
|
+
} & import("@floating-ui/react").ExtendedElements<import("@floating-ui/react").ReferenceType>;
|
|
49
|
+
context: {
|
|
50
|
+
y: number;
|
|
51
|
+
x: number;
|
|
52
|
+
placement: Placement;
|
|
53
|
+
strategy: import("@floating-ui/utils").Strategy;
|
|
54
|
+
middlewareData: import("@floating-ui/core").MiddlewareData;
|
|
55
|
+
isPositioned: boolean;
|
|
56
|
+
update: () => void;
|
|
57
|
+
floatingStyles: import("react").CSSProperties;
|
|
58
|
+
open: boolean;
|
|
59
|
+
onOpenChange: (open: boolean, event?: Event | undefined, reason?: import("@floating-ui/react").OpenChangeReason | undefined) => void;
|
|
60
|
+
events: import("@floating-ui/react").FloatingEvents;
|
|
61
|
+
dataRef: import("react").MutableRefObject<import("@floating-ui/react").ContextData>;
|
|
62
|
+
nodeId: string | undefined;
|
|
63
|
+
floatingId: string | undefined;
|
|
64
|
+
refs: import("@floating-ui/react").ExtendedRefs<import("@floating-ui/react").ReferenceType>;
|
|
65
|
+
elements: import("@floating-ui/react").ExtendedElements<import("@floating-ui/react").ReferenceType>;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { autoPlacement, autoUpdate, flip as flipMw, offset as offsetMw, shift as shiftMw, size as sizeMw, useFloating, } from '@floating-ui/react';
|
|
2
|
+
const hasResizeObserver = typeof window !== 'undefined' && 'ResizeObserver' in window;
|
|
3
|
+
function autoMiddlewareFor(p) {
|
|
4
|
+
if (!String(p).startsWith('auto'))
|
|
5
|
+
return null;
|
|
6
|
+
const alignment = p === 'auto-start' ? 'start' : p === 'auto-end' ? 'end' : undefined;
|
|
7
|
+
return autoPlacement({ alignment });
|
|
8
|
+
}
|
|
9
|
+
function toOffsetMw(opt) {
|
|
10
|
+
if (opt == null)
|
|
11
|
+
return null;
|
|
12
|
+
if (Array.isArray(opt)) {
|
|
13
|
+
const [crossAxis, mainAxis] = opt;
|
|
14
|
+
return offsetMw({ crossAxis, mainAxis });
|
|
15
|
+
}
|
|
16
|
+
if (typeof opt === 'number')
|
|
17
|
+
return offsetMw(opt);
|
|
18
|
+
return offsetMw(opt);
|
|
19
|
+
}
|
|
20
|
+
export function usePopoverPosition({ allowFlip = true, allowShift = true, autoUpdateOptions, fitAvailableSpace = false, freeze = false, offset, placement = 'bottom-start', }) {
|
|
21
|
+
const autoMw = autoMiddlewareFor(placement);
|
|
22
|
+
const offsetMiddleware = toOffsetMw(offset);
|
|
23
|
+
const isSidePlacement = placement.startsWith('left') || placement.startsWith('right');
|
|
24
|
+
const middleware = [
|
|
25
|
+
// offset first (mirrors common Popper setups)
|
|
26
|
+
...(offsetMiddleware ? [offsetMiddleware] : []),
|
|
27
|
+
// choose between autoPlacement (Popper's "auto*") OR flip()
|
|
28
|
+
// only allow flip when not explicitly 'left*' or 'right*'
|
|
29
|
+
...(autoMw ? [autoMw] : allowFlip && !isSidePlacement ? [flipMw()] : []),
|
|
30
|
+
// viewport collision adjustments
|
|
31
|
+
...(allowShift ? [shiftMw({ padding: 8 })] : []),
|
|
32
|
+
// optional size constraining
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
34
|
+
...(fitAvailableSpace ? [sizeMw({ apply: () => { } })] : []),
|
|
35
|
+
];
|
|
36
|
+
// if placement is 'auto*', seed with any static placement; autoPlacement will pick the final one
|
|
37
|
+
const seedPlacement = String(placement).startsWith('auto')
|
|
38
|
+
? 'bottom'
|
|
39
|
+
: placement;
|
|
40
|
+
return useFloating({
|
|
41
|
+
middleware,
|
|
42
|
+
placement: seedPlacement,
|
|
43
|
+
strategy: 'fixed',
|
|
44
|
+
whileElementsMounted: freeze
|
|
45
|
+
? undefined
|
|
46
|
+
: (reference, floating, update) => autoUpdate(reference, floating, update, {
|
|
47
|
+
ancestorResize: true,
|
|
48
|
+
ancestorScroll: true,
|
|
49
|
+
animationFrame: false,
|
|
50
|
+
elementResize: hasResizeObserver,
|
|
51
|
+
...autoUpdateOptions,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PropsWithChildren } from 'react';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import type {
|
|
3
|
+
import type { PopperLikePlacement } from '../Dialog';
|
|
4
4
|
type DropdownContextValue = {
|
|
5
5
|
close(): void;
|
|
6
6
|
};
|
|
@@ -8,7 +8,7 @@ export declare const useDropdownContext: () => DropdownContextValue;
|
|
|
8
8
|
export type DropdownProps = PropsWithChildren<{
|
|
9
9
|
className?: string;
|
|
10
10
|
openButtonProps?: React.HTMLAttributes<HTMLButtonElement>;
|
|
11
|
-
placement?:
|
|
11
|
+
placement?: PopperLikePlacement;
|
|
12
12
|
}>;
|
|
13
13
|
export declare const Dropdown: (props: DropdownProps) => React.JSX.Element;
|
|
14
14
|
export {};
|
|
@@ -116,5 +116,5 @@ export const Message = (props) => {
|
|
|
116
116
|
notify: addNotification,
|
|
117
117
|
});
|
|
118
118
|
const highlighted = highlightedMessageId === message.id;
|
|
119
|
-
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
|
+
return (React.createElement(MemoizedMessage, { additionalMessageInputProps: props.additionalMessageInputProps, autoscrollToBottom: props.autoscrollToBottom, canPin: canPin, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: props.customMessageActions, deliveredTo: props.deliveredTo, 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 }));
|
|
120
120
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useTranslationContext } from '../../context/TranslationContext';
|
|
3
|
+
import { useChannelStateContext } from '../../context';
|
|
3
4
|
const UnMemoizedMessageRepliesCountButton = (props) => {
|
|
4
5
|
const { labelPlural, labelSingle, onClick, reply_count = 0 } = props;
|
|
6
|
+
const { channelCapabilities } = useChannelStateContext();
|
|
5
7
|
const { t } = useTranslationContext('MessageRepliesCountButton');
|
|
6
8
|
if (!reply_count)
|
|
7
9
|
return null;
|
|
@@ -13,6 +15,6 @@ const UnMemoizedMessageRepliesCountButton = (props) => {
|
|
|
13
15
|
replyCountText = `1 ${labelSingle}`;
|
|
14
16
|
}
|
|
15
17
|
return (React.createElement("div", { className: 'str-chat__message-replies-count-button-wrapper' },
|
|
16
|
-
React.createElement("button", { className: 'str-chat__message-replies-count-button', "data-testid": 'replies-count-button', onClick: onClick }, replyCountText)));
|
|
18
|
+
React.createElement("button", { className: 'str-chat__message-replies-count-button', "data-testid": 'replies-count-button', disabled: !channelCapabilities['send-reply'], onClick: onClick }, replyCountText)));
|
|
17
19
|
};
|
|
18
20
|
export const MessageRepliesCountButton = React.memo(UnMemoizedMessageRepliesCountButton);
|
|
@@ -6,6 +6,7 @@ export type MessageStatusProps = {
|
|
|
6
6
|
MessageDeliveredStatus?: React.ComponentType;
|
|
7
7
|
MessageReadStatus?: React.ComponentType;
|
|
8
8
|
MessageSendingStatus?: React.ComponentType;
|
|
9
|
+
MessageSentStatus?: React.ComponentType;
|
|
9
10
|
messageType?: string;
|
|
10
11
|
tooltipUserNameMapper?: TooltipUsernameMapper;
|
|
11
12
|
};
|