diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index d15533eed..adfd3e738 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -96,22 +96,4 @@ const Threads = () => { ); }; -/* {activeThread && ( - - - - - - )} */ - -// const Wrapper = () => { -// const { activeThread } = useThreadContext(); - -// console.log({ activeThread }); - -// if (!activeThread) return; - -// return ; -// }; - export default App; diff --git a/examples/vite/src/index.scss b/examples/vite/src/index.scss index 082c23e3f..fee29480a 100644 --- a/examples/vite/src/index.scss +++ b/examples/vite/src/index.scss @@ -16,10 +16,10 @@ body, display: flex; height: 100%; - .str-chat__thread-list { - width: 50%; - height: 100%; - } + // .str-chat__thread-list { + // width: 50%; + // height: 100%; + // } .str-chat__thread-list-item { all: unset; @@ -99,7 +99,7 @@ body, .str-chat__thread { width: 100%; height: 100%; - position: fixed; + position: absolute; z-index: 1; } @@ -122,6 +122,18 @@ body, } } + .str-chat.threads { + display: flex; + height: 100%; + width: 100%; + + .vml { + display: flex; + flex-direction: column; + width: 70%; + } + } + @media screen and (min-width: 768px) { //.str-chat__channel-list.thread-open { // &.menu-open { @@ -146,11 +158,6 @@ body, position: initial; z-index: 0; } - - .str-chat__thread { - position: initial; - z-index: 0; - } } @media screen and (min-width: 1024px) { @@ -162,8 +169,8 @@ body, .str-chat__thread { width: 45%; - //position: initial; - //z-index: 0; + position: initial; + z-index: 0; } .str-chat__channel-header .str-chat__header-hamburger { diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index 0f6b2f9f4..2779bea5a 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -98,6 +98,7 @@ import type { URLEnrichmentConfig } from '../MessageInput/hooks/useLinkPreviews' import { defaultReactionOptions, ReactionOptions } from '../Reactions'; import { EventComponent } from '../EventComponent'; import { DateSeparator } from '../DateSeparator'; +import { useThreadContext } from '../Threads'; type ChannelPropsForwardedToComponentContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics @@ -399,6 +400,8 @@ const ChannelInner = < windowsEmojiClass, } = useChannelContainerClasses({ customClasses }); + const thread = useThreadContext(); + const [channelConfig, setChannelConfig] = useState(channel.getConfig()); const [notifications, setNotifications] = useState([]); const [quotedMessage, setQuotedMessage] = useState>(); @@ -1055,20 +1058,22 @@ const ChannelInner = < channel.state.filterErrorMessages(); const messagePreview = { - __html: text, attachments, created_at: new Date(), html: text, id: customMessageData?.id ?? `${client.userID}-${nanoid()}`, mentioned_users, + parent_id: parent?.id, reactions: [], status: 'sending', text, type: 'regular', user: client.user, - ...(parent?.id ? { parent_id: parent.id } : null), }; + // @ts-expect-error + if (thread.channel) thread.upsertReply({ message: messagePreview }); + updateMessage(messagePreview); await doSendMessage(messagePreview, customMessageData, options); diff --git a/src/components/Message/hooks/useReactionHandler.ts b/src/components/Message/hooks/useReactionHandler.ts index e057dceb6..7a0349890 100644 --- a/src/components/Message/hooks/useReactionHandler.ts +++ b/src/components/Message/hooks/useReactionHandler.ts @@ -11,6 +11,8 @@ import type { Reaction, ReactionResponse } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; +import { useThreadContext } from '../../Threads'; + export const reactionHandlerWarning = `Reaction handler was called, but it is missing one of its required arguments. Make sure the ChannelAction and ChannelState contexts are properly set and the hook is initialized with a valid message.`; @@ -19,6 +21,7 @@ export const useReactionHandler = < >( message?: StreamMessage, ) => { + const thread = useThreadContext(); const { updateMessage } = useChannelActionContext('useReactionHandler'); const { channel, channelCapabilities } = useChannelStateContext( 'useReactionHandler', @@ -93,15 +96,18 @@ export const useReactionHandler = < try { updateMessage(tempMessage); + if (thread.channel) thread.upsertReply({ message: tempMessage }); const messageResponse = add ? await channel.sendReaction(id, { type } as Reaction) : await channel.deleteReaction(id, type); + // seems useless as we're expecting WS event to come in and replace this anyway updateMessage(messageResponse.message); } catch (error) { // revert to the original message if the API call fails updateMessage(message); + if (thread.channel) thread.upsertReply({ message }); } }, 1000); diff --git a/src/components/Thread/Thread.tsx b/src/components/Thread/Thread.tsx index 448b678ba..a21df4169 100644 --- a/src/components/Thread/Thread.tsx +++ b/src/components/Thread/Thread.tsx @@ -105,7 +105,7 @@ const ThreadInner = < thread, threadHasMore, threadLoadingMore, - threadMessages, + threadMessages = [], threadSuppressAutoscroll, } = useChannelStateContext('Thread'); const { closeThread, loadMoreThread } = useChannelActionContext('Thread'); @@ -164,7 +164,7 @@ const ThreadInner = < loadMore={parentMessage ? threadInstance.loadPreviousPage : loadMoreThread} Message={MessageUIComponent} messageActions={messageActions} - messages={threadInstance.channel ? latestReplies : threadMessages ?? []} + messages={parentMessage ? latestReplies : threadMessages} suppressAutoscroll={threadSuppressAutoscroll} threadList {...(virtualized ? additionalVirtualizedMessageListProps : additionalMessageListProps)} diff --git a/src/components/Threads/ThreadContext.tsx b/src/components/Threads/ThreadContext.tsx index 86225e082..5be9ab568 100644 --- a/src/components/Threads/ThreadContext.tsx +++ b/src/components/Threads/ThreadContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useMemo } from 'react'; import { Channel } from '../../components'; @@ -6,6 +6,11 @@ import type { PropsWithChildren } from 'react'; import { Thread } from 'stream-chat'; import { useChatContext } from '../../context'; +/** + * TODO: + * - make it easier for current state of the SDK to use thread methods (meaning thread should be fully initialized and checks like `thread.channel && ...` shouldn't exist (these checks are due to a "placeholder" which is used anytime components are utilised under normal circumstances - not under ThreadContext) + */ + export type ThreadContextValue = Thread | undefined; export const ThreadContext = createContext(undefined); @@ -14,7 +19,12 @@ export const useThreadContext = () => { const { client } = useChatContext(); const thread = useContext(ThreadContext); - if (!thread) return new Thread({ client, registerEventHandlers: false, threadData: {} }); + const placeholder = useMemo( + () => new Thread({ client, registerEventHandlers: false, threadData: {} }), + [client], + ); + + if (!thread) return placeholder; return thread; }; @@ -25,17 +35,6 @@ export const ThreadProvider = ({ children, thread }: PropsWithChildren<{ thread? ); -// export const ThreadFacilitator = ({ -// children, -// thread, -// }: PropsWithChildren<{ -// thread: Thread; -// }>) => { -// -// {children}; -// ; -// }; - /** * * client { diff --git a/src/components/Threads/ThreadList/ThreadList.tsx b/src/components/Threads/ThreadList/ThreadList.tsx index 6e744b35b..4b67d465a 100644 --- a/src/components/Threads/ThreadList/ThreadList.tsx +++ b/src/components/Threads/ThreadList/ThreadList.tsx @@ -1,9 +1,8 @@ import React, { useEffect } from 'react'; import { ComputeItemKey, Virtuoso } from 'react-virtuoso'; -import { StreamChat } from 'stream-chat'; import type { ComponentType, PointerEvent } from 'react'; -import type { InferStoreValueType, Thread } from 'stream-chat'; +import type { InferStoreValueType, Thread, ThreadManager } from 'stream-chat'; import { ThreadListItem } from './ThreadListItem'; import { useChatContext } from '../../../context'; @@ -11,19 +10,29 @@ import { useSimpleStateStore } from '../hooks/useSimpleStateStore'; import type { ThreadListItemProps } from './ThreadListItem'; -const selector = (v: InferStoreValueType) => [v.threads] as const; +/** + * TODO: + * - add virtuosoProps override \w support for supplying custom threads array (typed virtuoso props, Thread[] only) + * - register event handlers of each of the threads (ThreadManager - threads.EventHandlers(), maybe simplify API) + * - add Header with re-query button + logic to ThreadManager + * - add Footer with "Loading" + * - move str-chat someplace else, think of a different name (str-chat__thread-list already used) + * - move itemContent to a component instead + * + * NICE TO HAVE: + * - probably good idea to move component context up to a Chat component + */ + +const selector = (nextValue: InferStoreValueType) => [nextValue.threads] as const; const computeItemKey: ComputeItemKey = (_, item) => item.id; type ThreadListProps = { onItemPointerDown?: (event: PointerEvent, thread: Thread) => void; ThreadListItem?: ComponentType; - // TODO: should support supplying custom threads array // threads?: Thread[] }; -// TODO: probably good idea to move component context up to a Chat component - export const ThreadList = ({ ThreadListItem: PropsThreadListItem = ThreadListItem, onItemPointerDown, @@ -37,11 +46,7 @@ export const ThreadList = ({ return ( - // TODO: figure out - handle next page load blocking client-side or here? - atBottom && client.threads.loadNextPage() - } - // TODO: str-chat class name does not belong here, str-chat__thread-list is already used (FUCK ME SIDEWAYS) + atBottomStateChange={(atBottom) => atBottom && client.threads.loadNextPage()} className='str-chat str-chat__thread-list' computeItemKey={computeItemKey} data={threads}