stream-chat-react 12.0.0-rc.10 → 12.0.0-rc.12
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/Avatar/Avatar.js +5 -1
- package/dist/components/Channel/Channel.d.ts +3 -4
- package/dist/components/Channel/Channel.js +76 -24
- package/dist/components/Chat/hooks/useChat.js +10 -6
- package/dist/components/ChatView/ChatView.d.ts +18 -0
- package/dist/components/ChatView/ChatView.js +100 -0
- package/dist/components/ChatView/index.d.ts +1 -0
- package/dist/components/ChatView/index.js +1 -0
- package/dist/components/Message/Message.js +2 -1
- package/dist/components/Message/MessageOptions.js +3 -4
- package/dist/components/Message/MessageSimple.js +2 -1
- package/dist/components/Message/QuotedMessage.js +2 -1
- package/dist/components/Message/hooks/useReactionHandler.js +7 -0
- package/dist/components/Message/utils.d.ts +10 -1
- package/dist/components/Message/utils.js +16 -7
- package/dist/components/MessageActions/MessageActions.js +14 -9
- package/dist/components/MessageInput/MessageInputFlat.js +2 -2
- package/dist/components/MessageInput/QuotedMessagePreview.js +2 -1
- package/dist/components/MessageList/MessageList.js +1 -3
- package/dist/components/MessageList/VirtualizedMessageList.d.ts +2 -1
- package/dist/components/MessageList/VirtualizedMessageList.js +5 -2
- package/dist/components/MessageList/VirtualizedMessageListComponents.d.ts +1 -1
- package/dist/components/MessageList/VirtualizedMessageListComponents.js +6 -6
- package/dist/components/MessageList/renderMessages.d.ts +2 -2
- package/dist/components/MessageList/renderMessages.js +4 -1
- package/dist/components/Reactions/ReactionSelector.d.ts +5 -2
- package/dist/components/Reactions/ReactionSelector.js +2 -1
- package/dist/components/Reactions/ReactionsList.d.ts +4 -1
- package/dist/components/Reactions/hooks/useProcessReactions.js +2 -1
- package/dist/components/Thread/Thread.js +37 -10
- package/dist/components/Threads/ThreadContext.d.ts +9 -0
- package/dist/components/Threads/ThreadContext.js +9 -0
- package/dist/components/Threads/ThreadList/ThreadList.d.ts +9 -0
- package/dist/components/Threads/ThreadList/ThreadList.js +41 -0
- package/dist/components/Threads/ThreadList/ThreadListEmptyPlaceholder.d.ts +2 -0
- package/dist/components/Threads/ThreadList/ThreadListEmptyPlaceholder.js +5 -0
- package/dist/components/Threads/ThreadList/ThreadListItem.d.ts +9 -0
- package/dist/components/Threads/ThreadList/ThreadListItem.js +52 -0
- package/dist/components/Threads/ThreadList/ThreadListItemUI.d.ts +18 -0
- package/dist/components/Threads/ThreadList/ThreadListItemUI.js +76 -0
- package/dist/components/Threads/ThreadList/ThreadListLoadingIndicator.d.ts +2 -0
- package/dist/components/Threads/ThreadList/ThreadListLoadingIndicator.js +14 -0
- package/dist/components/Threads/ThreadList/ThreadListUnseenThreadsBanner.d.ts +2 -0
- package/dist/components/Threads/ThreadList/ThreadListUnseenThreadsBanner.js +16 -0
- package/dist/components/Threads/ThreadList/index.d.ts +3 -0
- package/dist/components/Threads/ThreadList/index.js +3 -0
- package/dist/components/Threads/UnreadCountBadge.d.ts +6 -0
- package/dist/components/Threads/UnreadCountBadge.js +5 -0
- package/dist/components/Threads/hooks/useStateStore.d.ts +3 -0
- package/dist/components/Threads/hooks/useStateStore.js +15 -0
- package/dist/components/Threads/hooks/useThreadManagerState.d.ts +2 -0
- package/dist/components/Threads/hooks/useThreadManagerState.js +6 -0
- package/dist/components/Threads/hooks/useThreadState.d.ts +5 -0
- package/dist/components/Threads/hooks/useThreadState.js +11 -0
- package/dist/components/Threads/icons.d.ts +8 -0
- package/dist/components/Threads/icons.js +13 -0
- package/dist/components/Threads/index.d.ts +3 -0
- package/dist/components/Threads/index.js +3 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/context/ComponentContext.d.ts +15 -40
- package/dist/context/ComponentContext.js +7 -9
- package/dist/context/MessageContext.d.ts +1 -1
- package/dist/context/MessageContext.js +3 -2
- package/dist/context/WithComponents.d.ts +5 -0
- package/dist/context/WithComponents.js +7 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +1 -0
- package/dist/css/v2/index.css +2 -2
- package/dist/css/v2/index.layout.css +2 -2
- package/dist/index.browser.cjs +6456 -5951
- package/dist/index.browser.cjs.map +4 -4
- package/dist/index.node.cjs +6390 -5870
- package/dist/index.node.cjs.map +4 -4
- package/dist/scss/v2/Avatar/Avatar-layout.scss +10 -2
- package/dist/scss/v2/Avatar/Avatar-theme.scss +5 -0
- package/dist/scss/v2/ChatView/ChatView-layout.scss +43 -0
- package/dist/scss/v2/ChatView/ChatView-theme.scss +31 -0
- package/dist/scss/v2/LoadingIndicator/LoadingIndicator-layout.scss +16 -0
- package/dist/scss/v2/MessageList/MessageList-layout.scss +0 -6
- package/dist/scss/v2/MessageList/VirtualizedMessageList-layout.scss +0 -12
- package/dist/scss/v2/Thread/Thread-layout.scss +15 -1
- package/dist/scss/v2/ThreadList/ThreadList-layout.scss +149 -0
- package/dist/scss/v2/ThreadList/ThreadList-theme.scss +74 -0
- package/dist/scss/v2/UnreadCountBadge/UnreadCountBadge-layout.scss +49 -0
- package/dist/scss/v2/UnreadCountBadge/UnreadCountBadge-theme.scss +10 -0
- package/dist/scss/v2/index.layout.scss +3 -0
- package/dist/scss/v2/index.scss +3 -0
- package/package.json +4 -4
|
@@ -20,7 +20,7 @@ import { useMessageInputContext } from '../../context/MessageInputContext';
|
|
|
20
20
|
import { useComponentContext } from '../../context/ComponentContext';
|
|
21
21
|
export const MessageInputFlat = () => {
|
|
22
22
|
const { t } = useTranslationContext('MessageInputFlat');
|
|
23
|
-
const { asyncMessagesMultiSendEnabled, attachments, cooldownRemaining, findAndEnqueueURLsToEnrich, handleSubmit, hideSendButton, isUploadEnabled, linkPreviews, maxFilesLeft, message, numberOfUploads, recordingController, setCooldownRemaining, text, uploadNewFiles, } = useMessageInputContext('MessageInputFlat');
|
|
23
|
+
const { asyncMessagesMultiSendEnabled, attachments, cooldownRemaining, findAndEnqueueURLsToEnrich, handleSubmit, hideSendButton, isUploadEnabled, linkPreviews, maxFilesLeft, message, numberOfUploads, parent, recordingController, setCooldownRemaining, text, uploadNewFiles, } = useMessageInputContext('MessageInputFlat');
|
|
24
24
|
const { AudioRecorder = DefaultAudioRecorder, AttachmentPreviewList = DefaultAttachmentPreviewList, CooldownTimer = DefaultCooldownTimer, FileUploadIcon = DefaultUploadIcon, LinkPreviewList = DefaultLinkPreviewList, QuotedMessagePreview = DefaultQuotedMessagePreview, RecordingPermissionDeniedNotification = DefaultRecordingPermissionDeniedNotification, SendButton = DefaultSendButton, StartRecordingAudioButton = DefaultStartRecordingAudioButton, EmojiPicker, } = useComponentContext('MessageInputFlat');
|
|
25
25
|
const { acceptedFiles = [], multipleUploads, quotedMessage, } = useChannelStateContext('MessageInputFlat');
|
|
26
26
|
const { setQuotedMessage } = useChannelActionContext('MessageInputFlat');
|
|
@@ -64,7 +64,7 @@ export const MessageInputFlat = () => {
|
|
|
64
64
|
return React.createElement(AudioRecorder, null);
|
|
65
65
|
// TODO: "!message" condition is a temporary fix for shared
|
|
66
66
|
// state when editing a message (fix shared state issue)
|
|
67
|
-
const displayQuotedMessage = !message && quotedMessage &&
|
|
67
|
+
const displayQuotedMessage = !message && quotedMessage && quotedMessage.parent_id === parent?.id;
|
|
68
68
|
const recordingEnabled = !!(recordingController.recorder && navigator.mediaDevices); // account for requirement on iOS as per this bug report: https://bugs.webkit.org/show_bug.cgi?id=252303
|
|
69
69
|
const isRecording = !!recordingController.recordingState;
|
|
70
70
|
return (React.createElement(React.Fragment, null,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
+
import { Attachment as DefaultAttachment } from '../Attachment';
|
|
2
3
|
import { Avatar as DefaultAvatar } from '../Avatar';
|
|
3
4
|
import { CloseIcon } from './icons';
|
|
4
5
|
import { useChannelActionContext } from '../../context/ChannelActionContext';
|
|
@@ -13,7 +14,7 @@ export const QuotedMessagePreviewHeader = () => {
|
|
|
13
14
|
React.createElement(CloseIcon, null))));
|
|
14
15
|
};
|
|
15
16
|
export const QuotedMessagePreview = ({ quotedMessage, }) => {
|
|
16
|
-
const { Attachment, Avatar = DefaultAvatar } = useComponentContext('QuotedMessagePreview');
|
|
17
|
+
const { Attachment = DefaultAttachment, Avatar = DefaultAvatar, } = useComponentContext('QuotedMessagePreview');
|
|
17
18
|
const { userLanguage } = useTranslationContext('QuotedMessagePreview');
|
|
18
19
|
const quotedMessageText = quotedMessage.i18n?.[`${userLanguage}_text`] ||
|
|
19
20
|
quotedMessage.text;
|
|
@@ -127,9 +127,7 @@ const MessageListWithContext = (props) => {
|
|
|
127
127
|
return (React.createElement(MessageListContextProvider, { value: { listElement, scrollToBottom } },
|
|
128
128
|
React.createElement(MessageListMainPanel, null,
|
|
129
129
|
!threadList && showUnreadMessagesNotification && (React.createElement(UnreadMessagesNotification, { unreadCount: channelUnreadUiState?.unread_messages })),
|
|
130
|
-
React.createElement("div", { className: clsx(messageListClass, {
|
|
131
|
-
[customClasses?.threadList || 'str-chat__thread-list']: threadList,
|
|
132
|
-
}), onScroll: onScroll, ref: setListElement, tabIndex: 0 }, showEmptyStateIndicator ? (React.createElement(EmptyStateIndicator, { key: 'empty-state-indicator', listType: threadList ? 'thread' : 'message' })) : (React.createElement(InfiniteScroll, { className: 'str-chat__message-list-scroll', "data-testid": 'reverse-infinite-scroll', hasNextPage: props.hasMoreNewer, hasPreviousPage: props.hasMore, head: props.head, isLoading: props.loadingMore, loader: React.createElement("div", { className: 'str-chat__list__loading', key: 'loading-indicator' }, props.loadingMore && React.createElement(LoadingIndicator, { size: 20 })), loadNextPage: loadMoreNewer, loadPreviousPage: loadMore, threshold: loadMoreScrollThreshold, ...restInternalInfiniteScrollProps },
|
|
130
|
+
React.createElement("div", { className: clsx(messageListClass, customClasses?.threadList), onScroll: onScroll, ref: setListElement, tabIndex: 0 }, showEmptyStateIndicator ? (React.createElement(EmptyStateIndicator, { listType: threadList ? 'thread' : 'message' })) : (React.createElement(InfiniteScroll, { className: 'str-chat__message-list-scroll', "data-testid": 'reverse-infinite-scroll', hasNextPage: props.hasMoreNewer, hasPreviousPage: props.hasMore, head: props.head, isLoading: props.loadingMore, loader: React.createElement("div", { className: 'str-chat__list__loading', key: 'loading-indicator' }, props.loadingMore && React.createElement(LoadingIndicator, { size: 20 })), loadNextPage: loadMoreNewer, loadPreviousPage: loadMore, threshold: loadMoreScrollThreshold, ...restInternalInfiniteScrollProps },
|
|
133
131
|
React.createElement("ul", { className: 'str-chat__ul', ref: setUlElement }, elements),
|
|
134
132
|
React.createElement(TypingIndicator, { threadList: threadList }),
|
|
135
133
|
React.createElement("div", { key: 'bottom' }))))),
|
|
@@ -3,7 +3,7 @@ import { ScrollSeekConfiguration, ScrollSeekPlaceholderProps, VirtuosoHandle, Vi
|
|
|
3
3
|
import { GroupStyle, ProcessMessagesParams } from './utils';
|
|
4
4
|
import { MessageProps, MessageUIComponentProps } from '../Message';
|
|
5
5
|
import { ChannelActionContextValue } from '../../context/ChannelActionContext';
|
|
6
|
-
import { StreamMessage } from '../../context/ChannelStateContext';
|
|
6
|
+
import { ChannelStateContextValue, StreamMessage } from '../../context/ChannelStateContext';
|
|
7
7
|
import { ChatContextValue } from '../../context/ChatContext';
|
|
8
8
|
import { ComponentContextValue } from '../../context/ComponentContext';
|
|
9
9
|
import type { UserResponse } from 'stream-chat';
|
|
@@ -40,6 +40,7 @@ type PropsDrilledToMessage = 'additionalMessageInputProps' | 'customMessageActio
|
|
|
40
40
|
export type VirtualizedMessageListProps<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = Partial<Pick<MessageProps<StreamChatGenerics>, PropsDrilledToMessage>> & {
|
|
41
41
|
/** Additional props to be passed the underlying [`react-virtuoso` virtualized list dependency](https://virtuoso.dev/virtuoso-api-reference/) */
|
|
42
42
|
additionalVirtuosoProps?: VirtuosoProps<UnknownType, VirtuosoContext<StreamChatGenerics>>;
|
|
43
|
+
channelUnreadUiState?: ChannelStateContextValue['channelUnreadUiState'];
|
|
43
44
|
/** If true, picking a reaction from the `ReactionSelector` component will close the selector */
|
|
44
45
|
closeReactionSelectorOnClick?: boolean;
|
|
45
46
|
/** Custom render function, if passed, certain UI props are ignored */
|
|
@@ -11,6 +11,9 @@ import { getGroupStyles, getLastReceived, processMessages, } from './utils';
|
|
|
11
11
|
import { MessageSimple } from '../Message';
|
|
12
12
|
import { UnreadMessagesNotification as DefaultUnreadMessagesNotification } from './UnreadMessagesNotification';
|
|
13
13
|
import { calculateFirstItemIndex, calculateItemIndex, EmptyPlaceholder, Header, Item, makeItemsRenderedHandler, messageRenderer, } from './VirtualizedMessageListComponents';
|
|
14
|
+
import { UnreadMessagesSeparator as DefaultUnreadMessagesSeparator } from '../MessageList';
|
|
15
|
+
import { DateSeparator as DefaultDateSeparator } from '../DateSeparator';
|
|
16
|
+
import { EventComponent as DefaultMessageSystem } from '../EventComponent';
|
|
14
17
|
import { useChannelActionContext, } from '../../context/ChannelActionContext';
|
|
15
18
|
import { useChannelStateContext, } from '../../context/ChannelStateContext';
|
|
16
19
|
import { useChatContext } from '../../context/ChatContext';
|
|
@@ -53,7 +56,7 @@ const VirtualizedMessageListWithContext = (props) => {
|
|
|
53
56
|
// Stops errors generated from react-virtuoso to bubble up
|
|
54
57
|
// to Sentry or other tracking tools.
|
|
55
58
|
useCaptureResizeObserverExceededError();
|
|
56
|
-
const { DateSeparator, GiphyPreviewMessage = DefaultGiphyPreviewMessage, MessageListNotifications = DefaultMessageListNotifications, MessageNotification = DefaultMessageNotification, MessageSystem, UnreadMessagesNotification = DefaultUnreadMessagesNotification, UnreadMessagesSeparator, VirtualMessage: MessageUIComponentFromContext = MessageSimple, TypingIndicator, } = useComponentContext('VirtualizedMessageList');
|
|
59
|
+
const { DateSeparator = DefaultDateSeparator, GiphyPreviewMessage = DefaultGiphyPreviewMessage, MessageListNotifications = DefaultMessageListNotifications, MessageNotification = DefaultMessageNotification, MessageSystem = DefaultMessageSystem, UnreadMessagesNotification = DefaultUnreadMessagesNotification, UnreadMessagesSeparator = DefaultUnreadMessagesSeparator, VirtualMessage: MessageUIComponentFromContext = MessageSimple, TypingIndicator, } = useComponentContext('VirtualizedMessageList');
|
|
57
60
|
const MessageUIComponent = MessageUIComponentFromProps || MessageUIComponentFromContext;
|
|
58
61
|
const { client, customClasses } = useChatContext('VirtualizedMessageList');
|
|
59
62
|
const virtuoso = useRef(null);
|
|
@@ -236,5 +239,5 @@ export function VirtualizedMessageList(props) {
|
|
|
236
239
|
const { jumpToLatestMessage, loadMore, loadMoreNewer, } = useChannelActionContext('VirtualizedMessageList');
|
|
237
240
|
const { channel, channelUnreadUiState, hasMore, hasMoreNewer, highlightedMessageId, loadingMore, loadingMoreNewer, messages: contextMessages, notifications, read, suppressAutoscroll, } = useChannelStateContext('VirtualizedMessageList');
|
|
238
241
|
const messages = props.messages || contextMessages;
|
|
239
|
-
return (React.createElement(VirtualizedMessageListWithContext, { channel: channel, channelUnreadUiState: channelUnreadUiState, hasMore: !!hasMore, hasMoreNewer: !!hasMoreNewer, highlightedMessageId: highlightedMessageId, jumpToLatestMessage: jumpToLatestMessage, loadingMore: !!loadingMore, loadingMoreNewer: !!loadingMoreNewer, loadMore: loadMore, loadMoreNewer: loadMoreNewer, messages: messages, notifications: notifications, read: read, suppressAutoscroll: suppressAutoscroll, ...props }));
|
|
242
|
+
return (React.createElement(VirtualizedMessageListWithContext, { channel: channel, channelUnreadUiState: props.channelUnreadUiState ?? channelUnreadUiState, hasMore: !!hasMore, hasMoreNewer: !!hasMoreNewer, highlightedMessageId: highlightedMessageId, jumpToLatestMessage: jumpToLatestMessage, loadingMore: !!loadingMore, loadingMoreNewer: !!loadingMoreNewer, loadMore: loadMore, loadMoreNewer: loadMoreNewer, messages: messages, notifications: notifications, read: read, suppressAutoscroll: suppressAutoscroll, ...props }));
|
|
240
243
|
}
|
|
@@ -11,7 +11,7 @@ type CommonVirtuosoComponentProps<StreamChatGenerics extends DefaultStreamChatGe
|
|
|
11
11
|
context?: VirtuosoContext<StreamChatGenerics>;
|
|
12
12
|
};
|
|
13
13
|
export declare const Item: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>({ context, ...props }: ItemProps & CommonVirtuosoComponentProps<StreamChatGenerics>) => React.JSX.Element;
|
|
14
|
-
export declare const Header: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>({ context, }: CommonVirtuosoComponentProps<StreamChatGenerics>) => React.JSX.Element
|
|
14
|
+
export declare const Header: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>({ context, }: CommonVirtuosoComponentProps<StreamChatGenerics>) => React.JSX.Element;
|
|
15
15
|
export declare const EmptyPlaceholder: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>({ context, }: CommonVirtuosoComponentProps<StreamChatGenerics>) => React.JSX.Element;
|
|
16
16
|
export declare const messageRenderer: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(virtuosoIndex: number, _data: UnknownType, virtuosoContext: VirtuosoContext<StreamChatGenerics>) => React.JSX.Element | null;
|
|
17
17
|
export {};
|
|
@@ -37,17 +37,17 @@ export const Item = ({ context, ...props }) => {
|
|
|
37
37
|
};
|
|
38
38
|
export const Header = ({ context, }) => {
|
|
39
39
|
const { LoadingIndicator = DefaultLoadingIndicator } = useComponentContext('VirtualizedMessageListHeader');
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
return (React.createElement(React.Fragment, null,
|
|
41
|
+
context?.head,
|
|
42
|
+
context?.loadingMore && LoadingIndicator && (React.createElement("div", { className: 'str-chat__virtual-list__loading' },
|
|
43
|
+
React.createElement(LoadingIndicator, { size: 20 })))));
|
|
44
44
|
};
|
|
45
45
|
export const EmptyPlaceholder = ({ context, }) => {
|
|
46
46
|
const { EmptyStateIndicator = DefaultEmptyStateIndicator, } = useComponentContext('VirtualizedMessageList');
|
|
47
47
|
return (React.createElement(React.Fragment, null, EmptyStateIndicator && (React.createElement(EmptyStateIndicator, { listType: context?.threadList ? 'thread' : 'message' }))));
|
|
48
48
|
};
|
|
49
49
|
export const messageRenderer = (virtuosoIndex, _data, virtuosoContext) => {
|
|
50
|
-
const { additionalMessageInputProps, closeReactionSelectorOnClick, customMessageActions, customMessageRenderer, DateSeparator, firstUnreadMessageId, formatDate, lastReadDate, lastReadMessageId, lastReceivedMessageId, Message: MessageUIComponent, messageActions, messageGroupStyles, MessageSystem, numItemsPrepended, ownMessagesReadByOthers, processedMessages: messageList, reactionDetailsSort, shouldGroupByUser, sortReactionDetails, sortReactions, unreadMessageCount = 0, UnreadMessagesSeparator, virtuosoRef, } = virtuosoContext;
|
|
50
|
+
const { additionalMessageInputProps, closeReactionSelectorOnClick, customMessageActions, customMessageRenderer, DateSeparator, firstUnreadMessageId, formatDate, lastReadDate, lastReadMessageId, lastReceivedMessageId, Message: MessageUIComponent, messageActions, messageGroupStyles, MessageSystem, numItemsPrepended, ownMessagesReadByOthers, processedMessages: messageList, reactionDetailsSort, shouldGroupByUser, sortReactionDetails, sortReactions, threadList, unreadMessageCount = 0, UnreadMessagesSeparator, virtuosoRef, } = virtuosoContext;
|
|
51
51
|
const streamMessageIndex = calculateItemIndex(virtuosoIndex, numItemsPrepended);
|
|
52
52
|
if (customMessageRenderer) {
|
|
53
53
|
return customMessageRenderer(messageList, streamMessageIndex);
|
|
@@ -89,7 +89,7 @@ export const messageRenderer = (virtuosoIndex, _data, virtuosoContext) => {
|
|
|
89
89
|
return (React.createElement(React.Fragment, null,
|
|
90
90
|
showUnreadSeparatorAbove && (React.createElement("div", { className: 'str-chat__unread-messages-separator-wrapper' },
|
|
91
91
|
React.createElement(UnreadMessagesSeparator, { unreadCount: unreadMessageCount }))),
|
|
92
|
-
React.createElement(Message, { additionalMessageInputProps: additionalMessageInputProps, autoscrollToBottom: virtuosoRef.current?.autoscrollToBottom, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: customMessageActions, endOfGroup: endOfGroup, firstOfGroup: firstOfGroup, formatDate: formatDate, groupedByUser: groupedByUser, groupStyles: [messageGroupStyles[message.id] ?? ''], lastReceivedId: lastReceivedMessageId, message: message, Message: MessageUIComponent, messageActions: messageActions, reactionDetailsSort: reactionDetailsSort, readBy: ownMessagesReadByOthers[message.id] || [], sortReactionDetails: sortReactionDetails, sortReactions: sortReactions }),
|
|
92
|
+
React.createElement(Message, { additionalMessageInputProps: additionalMessageInputProps, autoscrollToBottom: virtuosoRef.current?.autoscrollToBottom, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: customMessageActions, endOfGroup: endOfGroup, firstOfGroup: firstOfGroup, formatDate: formatDate, groupedByUser: groupedByUser, groupStyles: [messageGroupStyles[message.id] ?? ''], lastReceivedId: lastReceivedMessageId, message: message, Message: MessageUIComponent, messageActions: messageActions, reactionDetailsSort: reactionDetailsSort, readBy: ownMessagesReadByOthers[message.id] || [], sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: threadList }),
|
|
93
93
|
showUnreadSeparatorBelow && (React.createElement("div", { className: 'str-chat__unread-messages-separator-wrapper' },
|
|
94
94
|
React.createElement(UnreadMessagesSeparator, { unreadCount: unreadMessageCount })))));
|
|
95
95
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React, { ReactNode } from 'react';
|
|
2
|
+
import type { UserResponse } from 'stream-chat';
|
|
2
3
|
import { GroupStyle } from './utils';
|
|
3
|
-
import { MessageProps } from '../Message';
|
|
4
4
|
import { ComponentContextValue, CustomClasses } from '../../context';
|
|
5
|
-
import type { UserResponse } from 'stream-chat';
|
|
6
5
|
import type { ChannelUnreadUiState, DefaultStreamChatGenerics } from '../../types';
|
|
7
6
|
import type { StreamMessage } from '../../context/ChannelStateContext';
|
|
7
|
+
import type { MessageProps } from '../Message';
|
|
8
8
|
export interface RenderMessagesOptions<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> {
|
|
9
9
|
components: ComponentContextValue<StreamChatGenerics>;
|
|
10
10
|
lastReceivedMessageId: string | null;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import React, { Fragment } from 'react';
|
|
2
2
|
import { isDateSeparatorMessage } from './utils';
|
|
3
3
|
import { Message } from '../Message';
|
|
4
|
+
import { DateSeparator as DefaultDateSeparator } from '../DateSeparator';
|
|
5
|
+
import { EventComponent as DefaultMessageSystem } from '../EventComponent';
|
|
6
|
+
import { UnreadMessagesSeparator as DefaultUnreadMessagesSeparator } from './UnreadMessagesSeparator';
|
|
4
7
|
import { CUSTOM_MESSAGE_TYPE } from '../../constants/messageTypes';
|
|
5
8
|
export function defaultRenderMessages({ channelUnreadUiState, components, customClasses, lastReceivedMessageId: lastReceivedId, messageGroupStyles, messages, readData, sharedMessageProps: messageProps, }) {
|
|
6
|
-
const { DateSeparator, HeaderComponent, MessageSystem, UnreadMessagesSeparator } = components;
|
|
9
|
+
const { DateSeparator = DefaultDateSeparator, HeaderComponent, MessageSystem = DefaultMessageSystem, UnreadMessagesSeparator = DefaultUnreadMessagesSeparator, } = components;
|
|
7
10
|
const renderedMessages = [];
|
|
8
11
|
let firstMessage;
|
|
9
12
|
for (let index = 0; index < messages.length; index++) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { AvatarProps } from '../Avatar';
|
|
3
2
|
import type { ReactionGroupResponse, ReactionResponse } from 'stream-chat';
|
|
3
|
+
import type { AvatarProps } from '../Avatar';
|
|
4
4
|
import type { DefaultStreamChatGenerics } from '../../types/types';
|
|
5
5
|
import type { ReactionOptions } from './reactionOptions';
|
|
6
6
|
export type ReactionSelectorProps<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = {
|
|
@@ -21,7 +21,10 @@ export type ReactionSelectorProps<StreamChatGenerics extends DefaultStreamChatGe
|
|
|
21
21
|
reaction_counts?: Record<string, number>;
|
|
22
22
|
/** An object containing summary for each reaction type on a message */
|
|
23
23
|
reaction_groups?: Record<string, ReactionGroupResponse>;
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* @deprecated
|
|
26
|
+
* A list of the currently supported reactions on a message
|
|
27
|
+
* */
|
|
25
28
|
reactionOptions?: ReactionOptions;
|
|
26
29
|
/** If true, adds a CSS class that reverses the horizontal positioning of the selector */
|
|
27
30
|
reverse?: boolean;
|
|
@@ -4,9 +4,10 @@ import { isMutableRef } from './utils/utils';
|
|
|
4
4
|
import { Avatar as DefaultAvatar } from '../Avatar';
|
|
5
5
|
import { useComponentContext } from '../../context/ComponentContext';
|
|
6
6
|
import { useMessageContext } from '../../context/MessageContext';
|
|
7
|
+
import { defaultReactionOptions } from './reactionOptions';
|
|
7
8
|
const UnMemoizedReactionSelector = React.forwardRef((props, ref) => {
|
|
8
9
|
const { Avatar: propAvatar, detailedView = true, handleReaction: propHandleReaction, latest_reactions: propLatestReactions, own_reactions: propOwnReactions, reaction_groups: propReactionGroups, reactionOptions: propReactionOptions, reverse = false, } = props;
|
|
9
|
-
const { Avatar: contextAvatar, reactionOptions: contextReactionOptions, } = useComponentContext('ReactionSelector');
|
|
10
|
+
const { Avatar: contextAvatar, reactionOptions: contextReactionOptions = defaultReactionOptions, } = useComponentContext('ReactionSelector');
|
|
10
11
|
const { handleReaction: contextHandleReaction, message, } = useMessageContext('ReactionSelector');
|
|
11
12
|
const reactionOptions = propReactionOptions ?? contextReactionOptions;
|
|
12
13
|
const Avatar = propAvatar || contextAvatar || DefaultAvatar;
|
|
@@ -17,7 +17,10 @@ export type ReactionsListProps<StreamChatGenerics extends DefaultStreamChatGener
|
|
|
17
17
|
reaction_counts?: Record<string, number>;
|
|
18
18
|
/** An object containing summary for each reaction type on a message */
|
|
19
19
|
reaction_groups?: Record<string, ReactionGroupResponse>;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* @deprecated
|
|
22
|
+
* A list of the currently supported reactions on a message
|
|
23
|
+
* */
|
|
21
24
|
reactionOptions?: ReactionOptions;
|
|
22
25
|
/** An array of the reaction objects to display in the list */
|
|
23
26
|
reactions?: ReactionResponse<StreamChatGenerics>[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo } from 'react';
|
|
2
2
|
import { useComponentContext, useMessageContext } from '../../../context';
|
|
3
|
+
import { defaultReactionOptions } from '../reactionOptions';
|
|
3
4
|
export const defaultReactionsSort = (a, b) => {
|
|
4
5
|
if (a.firstReactionAt && b.firstReactionAt) {
|
|
5
6
|
return +a.firstReactionAt - +b.firstReactionAt;
|
|
@@ -9,7 +10,7 @@ export const defaultReactionsSort = (a, b) => {
|
|
|
9
10
|
export const useProcessReactions = (params) => {
|
|
10
11
|
const { own_reactions: propOwnReactions, reaction_groups: propReactionGroups, reactionOptions: propReactionOptions, reactions: propReactions, sortReactions: propSortReactions, } = params;
|
|
11
12
|
const { message, sortReactions: contextSortReactions } = useMessageContext('useProcessReactions');
|
|
12
|
-
const { reactionOptions: contextReactionOptions } = useComponentContext('useProcessReactions');
|
|
13
|
+
const { reactionOptions: contextReactionOptions = defaultReactionOptions, } = useComponentContext('useProcessReactions');
|
|
13
14
|
const reactionOptions = propReactionOptions ?? contextReactionOptions;
|
|
14
15
|
const sortReactions = propSortReactions ?? contextSortReactions ?? defaultReactionsSort;
|
|
15
16
|
const latestReactions = propReactions || message.latest_reactions;
|
|
@@ -6,19 +6,31 @@ import { MessageList, VirtualizedMessageList, } from '../MessageList';
|
|
|
6
6
|
import { ThreadHeader as DefaultThreadHeader } from './ThreadHeader';
|
|
7
7
|
import { ThreadHead as DefaultThreadHead } from '../Thread/ThreadHead';
|
|
8
8
|
import { useChannelActionContext, useChannelStateContext, useChatContext, useComponentContext, } from '../../context';
|
|
9
|
+
import { useStateStore, useThreadContext } from '../../components/Threads';
|
|
9
10
|
/**
|
|
10
11
|
* The Thread component renders a parent Message with a list of replies
|
|
11
12
|
*/
|
|
12
13
|
export const Thread = (props) => {
|
|
13
14
|
const { channel, channelConfig, thread } = useChannelStateContext('Thread');
|
|
14
|
-
|
|
15
|
+
const threadInstance = useThreadContext();
|
|
16
|
+
if ((!thread && !threadInstance) || channelConfig?.replies === false)
|
|
15
17
|
return null;
|
|
16
|
-
//
|
|
17
|
-
return
|
|
18
|
+
// the wrapper ensures a key variable is set and the component recreates on thread switch
|
|
19
|
+
return (
|
|
20
|
+
// FIXME: TS is having trouble here as at least one of the two would always be defined
|
|
21
|
+
React.createElement(ThreadInner, { ...props, key: `thread-${(thread ?? threadInstance)?.id}-${channel?.cid}` }));
|
|
18
22
|
};
|
|
23
|
+
const selector = (nextValue) => [
|
|
24
|
+
nextValue.replies,
|
|
25
|
+
nextValue.pagination.isLoadingPrev,
|
|
26
|
+
nextValue.pagination.isLoadingNext,
|
|
27
|
+
nextValue.parentMessage,
|
|
28
|
+
];
|
|
19
29
|
const ThreadInner = (props) => {
|
|
20
30
|
const { additionalMessageInputProps, additionalMessageListProps, additionalParentMessageProps, additionalVirtualizedMessageListProps, autoFocus = true, enableDateSeparator = false, Input: PropInput, Message: PropMessage, messageActions = Object.keys(MESSAGE_ACTIONS), virtualized, } = props;
|
|
21
|
-
const
|
|
31
|
+
const threadInstance = useThreadContext();
|
|
32
|
+
const [latestReplies, isLoadingPrev, isLoadingNext, parentMessage] = useStateStore(threadInstance?.state, selector) ?? [];
|
|
33
|
+
const { thread, threadHasMore, threadLoadingMore, threadMessages = [], threadSuppressAutoscroll, } = useChannelStateContext('Thread');
|
|
22
34
|
const { closeThread, loadMoreThread } = useChannelActionContext('Thread');
|
|
23
35
|
const { customClasses } = useChatContext('Thread');
|
|
24
36
|
const { ThreadInput: ContextInput, Message: ContextMessage, ThreadHead = DefaultThreadHead, ThreadHeader = DefaultThreadHeader, VirtualMessage, } = useComponentContext('Thread');
|
|
@@ -33,16 +45,31 @@ const ThreadInner = (props) => {
|
|
|
33
45
|
loadMoreThread();
|
|
34
46
|
}
|
|
35
47
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
|
-
}, []);
|
|
37
|
-
|
|
48
|
+
}, [thread, loadMoreThread]);
|
|
49
|
+
const threadProps = threadInstance
|
|
50
|
+
? {
|
|
51
|
+
loadingMore: isLoadingPrev,
|
|
52
|
+
loadingMoreNewer: isLoadingNext,
|
|
53
|
+
loadMore: threadInstance.loadPrevPage,
|
|
54
|
+
loadMoreNewer: threadInstance.loadNextPage,
|
|
55
|
+
messages: latestReplies,
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
hasMore: threadHasMore,
|
|
59
|
+
loadingMore: threadLoadingMore,
|
|
60
|
+
loadMore: loadMoreThread,
|
|
61
|
+
messages: threadMessages,
|
|
62
|
+
};
|
|
63
|
+
const messageAsThread = thread ?? parentMessage;
|
|
64
|
+
if (!messageAsThread)
|
|
38
65
|
return null;
|
|
39
66
|
const threadClass = customClasses?.thread ||
|
|
40
67
|
clsx('str-chat__thread-container str-chat__thread', {
|
|
41
68
|
'str-chat__thread--virtualized': virtualized,
|
|
42
69
|
});
|
|
43
|
-
const head = (React.createElement(ThreadHead, { key:
|
|
70
|
+
const head = (React.createElement(ThreadHead, { key: messageAsThread.id, message: messageAsThread, Message: MessageUIComponent, ...additionalParentMessageProps }));
|
|
44
71
|
return (React.createElement("div", { className: threadClass },
|
|
45
|
-
React.createElement(ThreadHeader, { closeThread: closeThread, thread:
|
|
46
|
-
React.createElement(ThreadMessageList, { disableDateSeparator: !enableDateSeparator,
|
|
47
|
-
React.createElement(MessageInput, { focus: autoFocus, Input: ThreadInput, parent: thread, publishTypingEvent: false, ...additionalMessageInputProps })));
|
|
72
|
+
React.createElement(ThreadHeader, { closeThread: closeThread, thread: messageAsThread }),
|
|
73
|
+
React.createElement(ThreadMessageList, { disableDateSeparator: !enableDateSeparator, head: head, Message: MessageUIComponent, messageActions: messageActions, suppressAutoscroll: threadSuppressAutoscroll, threadList: true, ...threadProps, ...(virtualized ? additionalVirtualizedMessageListProps : additionalMessageListProps) }),
|
|
74
|
+
React.createElement(MessageInput, { focus: autoFocus, Input: ThreadInput, parent: thread ?? parentMessage, publishTypingEvent: false, ...additionalMessageInputProps })));
|
|
48
75
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { PropsWithChildren } from 'react';
|
|
3
|
+
import { Thread } from 'stream-chat';
|
|
4
|
+
export type ThreadContextValue = Thread | undefined;
|
|
5
|
+
export declare const ThreadContext: React.Context<ThreadContextValue>;
|
|
6
|
+
export declare const useThreadContext: () => Thread<import("stream-chat").DefaultGenerics> | undefined;
|
|
7
|
+
export declare const ThreadProvider: ({ children, thread }: PropsWithChildren<{
|
|
8
|
+
thread?: Thread;
|
|
9
|
+
}>) => React.JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import { Channel } from '../../components';
|
|
3
|
+
export const ThreadContext = createContext(undefined);
|
|
4
|
+
export const useThreadContext = () => {
|
|
5
|
+
const thread = useContext(ThreadContext);
|
|
6
|
+
return thread ?? undefined;
|
|
7
|
+
};
|
|
8
|
+
export const ThreadProvider = ({ children, thread }) => (React.createElement(ThreadContext.Provider, { value: thread },
|
|
9
|
+
React.createElement(Channel, { channel: thread?.channel }, children)));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { VirtuosoProps } from 'react-virtuoso';
|
|
3
|
+
import type { Thread } from 'stream-chat';
|
|
4
|
+
type ThreadListProps = {
|
|
5
|
+
virtuosoProps?: VirtuosoProps<Thread, unknown>;
|
|
6
|
+
};
|
|
7
|
+
export declare const useThreadList: () => void;
|
|
8
|
+
export declare const ThreadList: ({ virtuosoProps }: ThreadListProps) => React.JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { Virtuoso } from 'react-virtuoso';
|
|
3
|
+
import { ThreadListItem as DefaultThreadListItem } from './ThreadListItem';
|
|
4
|
+
import { ThreadListEmptyPlaceholder as DefaultThreadListEmptyPlaceholder } from './ThreadListEmptyPlaceholder';
|
|
5
|
+
import { ThreadListUnseenThreadsBanner as DefaultThreadListUnseenThreadsBanner } from './ThreadListUnseenThreadsBanner';
|
|
6
|
+
import { ThreadListLoadingIndicator as DefaultThreadListLoadingIndicator } from './ThreadListLoadingIndicator';
|
|
7
|
+
import { useChatContext, useComponentContext } from '../../../context';
|
|
8
|
+
import { useStateStore } from '../hooks/useStateStore';
|
|
9
|
+
const selector = (nextValue) => [nextValue.threads];
|
|
10
|
+
const computeItemKey = (_, item) => item.id;
|
|
11
|
+
export const useThreadList = () => {
|
|
12
|
+
const { client } = useChatContext();
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const handleVisibilityChange = () => {
|
|
15
|
+
if (document.visibilityState === 'visible') {
|
|
16
|
+
client.threads.activate();
|
|
17
|
+
}
|
|
18
|
+
if (document.visibilityState === 'hidden') {
|
|
19
|
+
client.threads.deactivate();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
handleVisibilityChange();
|
|
23
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
24
|
+
return () => {
|
|
25
|
+
client.threads.deactivate();
|
|
26
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
27
|
+
};
|
|
28
|
+
}, [client]);
|
|
29
|
+
};
|
|
30
|
+
export const ThreadList = ({ virtuosoProps }) => {
|
|
31
|
+
const { client } = useChatContext();
|
|
32
|
+
const { ThreadListItem = DefaultThreadListItem, ThreadListEmptyPlaceholder = DefaultThreadListEmptyPlaceholder, ThreadListLoadingIndicator = DefaultThreadListLoadingIndicator, ThreadListUnseenThreadsBanner = DefaultThreadListUnseenThreadsBanner, } = useComponentContext();
|
|
33
|
+
const [threads] = useStateStore(client.threads.state, selector);
|
|
34
|
+
useThreadList();
|
|
35
|
+
return (React.createElement("div", { className: 'str-chat__thread-list-container' },
|
|
36
|
+
React.createElement(ThreadListUnseenThreadsBanner, null),
|
|
37
|
+
React.createElement(Virtuoso, { atBottomStateChange: (atBottom) => atBottom && client.threads.loadNextPage(), className: 'str-chat__thread-list', components: {
|
|
38
|
+
EmptyPlaceholder: ThreadListEmptyPlaceholder,
|
|
39
|
+
Footer: ThreadListLoadingIndicator,
|
|
40
|
+
}, computeItemKey: computeItemKey, data: threads, itemContent: (_, thread) => React.createElement(ThreadListItem, { thread: thread }), ...virtuosoProps })));
|
|
41
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Icon } from '../icons';
|
|
3
|
+
export const ThreadListEmptyPlaceholder = () => (React.createElement("div", { className: 'str-chat__thread-list-empty-placeholder' },
|
|
4
|
+
React.createElement(Icon.MessageBubble, null),
|
|
5
|
+
"No threads here yet..."));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Thread } from 'stream-chat';
|
|
3
|
+
import type { ThreadListItemUIProps } from './ThreadListItemUI';
|
|
4
|
+
export type ThreadListItemProps = {
|
|
5
|
+
thread: Thread;
|
|
6
|
+
threadListItemUIProps?: ThreadListItemUIProps;
|
|
7
|
+
};
|
|
8
|
+
export declare const useThreadListItemContext: () => Thread<import("stream-chat").DefaultGenerics> | undefined;
|
|
9
|
+
export declare const ThreadListItem: ({ thread, threadListItemUIProps }: ThreadListItemProps) => React.JSX.Element;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import { useComponentContext } from '../../../context';
|
|
3
|
+
import { ThreadListItemUI as DefaultThreadListItemUI } from './ThreadListItemUI';
|
|
4
|
+
const ThreadListItemContext = createContext(undefined);
|
|
5
|
+
export const useThreadListItemContext = () => useContext(ThreadListItemContext);
|
|
6
|
+
export const ThreadListItem = ({ thread, threadListItemUIProps }) => {
|
|
7
|
+
const { ThreadListItemUI = DefaultThreadListItemUI } = useComponentContext();
|
|
8
|
+
return (React.createElement(ThreadListItemContext.Provider, { value: thread },
|
|
9
|
+
React.createElement(ThreadListItemUI, { ...threadListItemUIProps })));
|
|
10
|
+
};
|
|
11
|
+
// const App = () => {
|
|
12
|
+
// const route = useRouter();
|
|
13
|
+
// return (
|
|
14
|
+
// <Chat>
|
|
15
|
+
// {route === '/channels' && (
|
|
16
|
+
// <Channel>
|
|
17
|
+
// <MessageList />
|
|
18
|
+
// <Thread />
|
|
19
|
+
// </Channel>
|
|
20
|
+
// )}
|
|
21
|
+
// {route === '/threads' && (
|
|
22
|
+
// <Threads>
|
|
23
|
+
// <ThreadList />
|
|
24
|
+
// <ThreadProvider>
|
|
25
|
+
// <Thread />
|
|
26
|
+
// </ThreadProvider>
|
|
27
|
+
// </Threads>
|
|
28
|
+
// )}
|
|
29
|
+
// </Chat>
|
|
30
|
+
// );
|
|
31
|
+
// };
|
|
32
|
+
// pre-built layout
|
|
33
|
+
{
|
|
34
|
+
/*
|
|
35
|
+
<Chat client={chatClient}>
|
|
36
|
+
<Views>
|
|
37
|
+
// has default
|
|
38
|
+
<ViewSelector onItemPointerDown={} />
|
|
39
|
+
<View.Chat>
|
|
40
|
+
<Channel>
|
|
41
|
+
<MessageList />
|
|
42
|
+
<MessageInput />
|
|
43
|
+
</Channel>
|
|
44
|
+
</View.Chat>
|
|
45
|
+
<View.Thread> <-- activeThread state
|
|
46
|
+
<ThreadList /> <-- uses context for click handler
|
|
47
|
+
<WrappedThread /> <-- ThreadProvider + Channel combo
|
|
48
|
+
</View.Thread>
|
|
49
|
+
</Views>
|
|
50
|
+
</Chat>;
|
|
51
|
+
*/
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ComponentPropsWithoutRef } from 'react';
|
|
3
|
+
export type ThreadListItemUIProps = ComponentPropsWithoutRef<'button'>;
|
|
4
|
+
/**
|
|
5
|
+
* TODO:
|
|
6
|
+
* - maybe hover state? ask design
|
|
7
|
+
* - move styling to CSS library and clean it up (separate layout and theme)
|
|
8
|
+
* - use Moment/DayJs for proper created_at formatting (replace toLocaleTimeString)
|
|
9
|
+
* - handle deleted message [in progress]
|
|
10
|
+
*/
|
|
11
|
+
export declare const attachmentTypeIconMap: {
|
|
12
|
+
readonly audio: "🔈";
|
|
13
|
+
readonly file: "📄";
|
|
14
|
+
readonly image: "📷";
|
|
15
|
+
readonly video: "🎥";
|
|
16
|
+
readonly voiceRecording: "🎙️";
|
|
17
|
+
};
|
|
18
|
+
export declare const ThreadListItemUI: (props: ThreadListItemUIProps) => React.JSX.Element;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Timestamp } from '../../Message/Timestamp';
|
|
4
|
+
import { Avatar } from '../../Avatar';
|
|
5
|
+
import { Icon } from '../icons';
|
|
6
|
+
import { UnreadCountBadge } from '../UnreadCountBadge';
|
|
7
|
+
import { useChatContext } from '../../../context';
|
|
8
|
+
import { useThreadsViewContext } from '../../ChatView';
|
|
9
|
+
import { useThreadListItemContext } from './ThreadListItem';
|
|
10
|
+
import { useStateStore } from '../hooks/useStateStore';
|
|
11
|
+
/**
|
|
12
|
+
* TODO:
|
|
13
|
+
* - maybe hover state? ask design
|
|
14
|
+
* - move styling to CSS library and clean it up (separate layout and theme)
|
|
15
|
+
* - use Moment/DayJs for proper created_at formatting (replace toLocaleTimeString)
|
|
16
|
+
* - handle deleted message [in progress]
|
|
17
|
+
*/
|
|
18
|
+
export const attachmentTypeIconMap = {
|
|
19
|
+
audio: '🔈',
|
|
20
|
+
file: '📄',
|
|
21
|
+
image: '📷',
|
|
22
|
+
video: '🎥',
|
|
23
|
+
voiceRecording: '🎙️',
|
|
24
|
+
};
|
|
25
|
+
// TODO: translations
|
|
26
|
+
const getTitleFromMessage = ({ currentUserId, message, }) => {
|
|
27
|
+
const attachment = message?.attachments?.at(0);
|
|
28
|
+
let attachmentIcon = '';
|
|
29
|
+
if (attachment) {
|
|
30
|
+
attachmentIcon +=
|
|
31
|
+
attachmentTypeIconMap[attachment.type ?? 'file'] ??
|
|
32
|
+
attachmentTypeIconMap.file;
|
|
33
|
+
}
|
|
34
|
+
const messageBelongsToCurrentUser = message?.user?.id === currentUserId;
|
|
35
|
+
if (message?.deleted_at && message.parent_id)
|
|
36
|
+
return clsx(messageBelongsToCurrentUser && 'You:', 'This reply was deleted.');
|
|
37
|
+
if (message?.deleted_at && !message.parent_id)
|
|
38
|
+
return clsx(messageBelongsToCurrentUser && 'You:', 'The source message was deleted.');
|
|
39
|
+
if (attachment?.type === 'voiceRecording')
|
|
40
|
+
return clsx(attachmentIcon, messageBelongsToCurrentUser && 'You:', 'Voice message');
|
|
41
|
+
return clsx(attachmentIcon, messageBelongsToCurrentUser && 'You:', message?.text || attachment?.fallback || 'N/A');
|
|
42
|
+
};
|
|
43
|
+
export const ThreadListItemUI = (props) => {
|
|
44
|
+
const { client } = useChatContext();
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
46
|
+
const thread = useThreadListItemContext();
|
|
47
|
+
const selector = useCallback((nextValue) => [
|
|
48
|
+
nextValue.replies.at(-1),
|
|
49
|
+
(client.userID && nextValue.read[client.userID]?.unreadMessageCount) || 0,
|
|
50
|
+
nextValue.parentMessage,
|
|
51
|
+
nextValue.channel,
|
|
52
|
+
nextValue.deletedAt,
|
|
53
|
+
], [client]);
|
|
54
|
+
const [latestReply, ownUnreadMessageCount, parentMessage, channelData, deletedAt] = useStateStore(thread.state, selector);
|
|
55
|
+
const { activeThread, setActiveThread } = useThreadsViewContext();
|
|
56
|
+
const avatarProps = deletedAt ? null : latestReply?.user;
|
|
57
|
+
return (React.createElement("button", { "aria-selected": activeThread === thread, className: 'str-chat__thread-list-item', "data-thread-id": thread.id, onClick: () => setActiveThread(thread), role: 'option', ...props },
|
|
58
|
+
React.createElement("div", { className: 'str-chat__thread-list-item__channel' },
|
|
59
|
+
React.createElement(Icon.MessageBubble, null),
|
|
60
|
+
React.createElement("div", { className: 'str-chat__thread-list-item__channel-text' }, channelData.data?.name || 'N/A')),
|
|
61
|
+
React.createElement("div", { className: 'str-chat__thread-list-item__parent-message' },
|
|
62
|
+
React.createElement("div", { className: 'str-chat__thread-list-item__parent-message-text' },
|
|
63
|
+
"replied to: ",
|
|
64
|
+
getTitleFromMessage({ message: parentMessage })),
|
|
65
|
+
!deletedAt && React.createElement(UnreadCountBadge, { count: ownUnreadMessageCount })),
|
|
66
|
+
React.createElement("div", { className: 'str-chat__thread-list-item__latest-reply' },
|
|
67
|
+
React.createElement(Avatar, { ...avatarProps }),
|
|
68
|
+
React.createElement("div", { className: 'str-chat__thread-list-item__latest-reply-details' },
|
|
69
|
+
!deletedAt && (React.createElement("div", { className: 'str-chat__thread-list-item__latest-reply-created-by' }, latestReply?.user?.name || latestReply?.user?.id || 'Unknown sender')),
|
|
70
|
+
React.createElement("div", { className: 'str-chat__thread-list-item__latest-reply-text-and-timestamp' },
|
|
71
|
+
React.createElement("div", { className: 'str-chat__thread-list-item__latest-reply-text' }, deletedAt
|
|
72
|
+
? 'This thread was deleted'
|
|
73
|
+
: getTitleFromMessage({ currentUserId: client.user?.id, message: latestReply })),
|
|
74
|
+
React.createElement("div", { className: 'str-chat__thread-list-item__latest-reply-timestamp' },
|
|
75
|
+
React.createElement(Timestamp, { timestamp: deletedAt ?? latestReply?.created_at })))))));
|
|
76
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LoadingIndicator as DefaultLoadingIndicator } from '../../Loading';
|
|
3
|
+
import { useChatContext, useComponentContext } from '../../../context';
|
|
4
|
+
import { useStateStore } from '../hooks/useStateStore';
|
|
5
|
+
const selector = (nextValue) => [nextValue.pagination.isLoadingNext];
|
|
6
|
+
export const ThreadListLoadingIndicator = () => {
|
|
7
|
+
const { LoadingIndicator = DefaultLoadingIndicator } = useComponentContext();
|
|
8
|
+
const { client } = useChatContext();
|
|
9
|
+
const [isLoadingNext] = useStateStore(client.threads.state, selector);
|
|
10
|
+
if (!isLoadingNext)
|
|
11
|
+
return null;
|
|
12
|
+
return (React.createElement("div", { className: 'str-chat__thread-list-loading-indicator' },
|
|
13
|
+
React.createElement(LoadingIndicator, null)));
|
|
14
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Icon } from '../icons';
|
|
3
|
+
import { useChatContext } from '../../../context';
|
|
4
|
+
import { useStateStore } from '../hooks/useStateStore';
|
|
5
|
+
const selector = (nextValue) => [nextValue.unseenThreadIds];
|
|
6
|
+
export const ThreadListUnseenThreadsBanner = () => {
|
|
7
|
+
const { client } = useChatContext();
|
|
8
|
+
const [unseenThreadIds] = useStateStore(client.threads.state, selector);
|
|
9
|
+
if (!unseenThreadIds.length)
|
|
10
|
+
return null;
|
|
11
|
+
return (React.createElement("div", { className: 'str-chat__unseen-threads-banner' },
|
|
12
|
+
unseenThreadIds.length,
|
|
13
|
+
" unread threads",
|
|
14
|
+
React.createElement("button", { className: 'str-chat__unseen-threads-banner__button', onClick: () => client.threads.reload() },
|
|
15
|
+
React.createElement(Icon.Reload, null))));
|
|
16
|
+
};
|