diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index 32fc8fe0fb..026811844c 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -81,7 +81,7 @@ import { findInMsgSetByDate, findInMsgSetById, makeAddNotifications } from './ut import { useThreadContext } from '../Threads'; import { getChannel } from '../../utils'; import type { - ChannelUnreadUiState, + // ChannelUnreadUiState, GiphyVersions, ImageAttachmentSizeHandler, VideoAttachmentSizeHandler, @@ -187,7 +187,7 @@ export type ChannelProps = ChannelPropsForwardedToComponentContext & { /** Custom action handler to override the default `channel.markRead` request function (advanced usage only) */ doMarkReadRequest?: ( channel: StreamChannel, - setChannelUnreadUiState?: (state: ChannelUnreadUiState) => void, + // setChannelUnreadUiState?: (state: ChannelUnreadUiState) => void, ) => Promise | void; /** Custom action handler to override the default `channel.sendMessage` request function (advanced usage only) */ doSendMessageRequest?: ( @@ -329,22 +329,22 @@ const ChannelInner = ( const [notifications, setNotifications] = useState([]); const notificationTimeouts = useRef>([]); - const [channelUnreadUiState, _setChannelUnreadUiState] = - useState(); - - const channelReducer = useMemo(() => makeChannelReducer(), []); - - const [state, dispatch] = useReducer( - channelReducer, - // channel.initialized === false if client.channel().query() was not called, e.g. ChannelList is not used - // => Channel will call channel.watch() in useLayoutEffect => state.loading is used to signal the watch() call state - { - ...initialState, - hasMore: channel.state.messagePagination.hasPrev, - loading: !channel.initialized, - messages: channel.state.messages, - }, - ); + // const [channelUnreadUiState, _setChannelUnreadUiState] = + // useState(); + + // const channelReducer = useMemo(() => makeChannelReducer(), []); + + // const [state, dispatch] = useReducer( + // channelReducer, + // // channel.initialized === false if client.channel().query() was not called, e.g. ChannelList is not used + // // => Channel will call channel.watch() in useLayoutEffect => state.loading is used to signal the watch() call state + // { + // ...initialState, + // hasMore: channel.state.messagePagination.hasPrev, + // loading: !channel.initialized, + // messages: channel.state.messages, + // }, + // ); const jumpToMessageFromSearch = useSearchFocusedMessage(); const isMounted = useIsMounted(); @@ -358,23 +358,23 @@ const ChannelInner = ( const channelCapabilitiesArray = channel.data?.own_capabilities as string[]; - const throttledCopyStateFromChannel = throttle( - () => dispatch({ channel, type: 'copyStateFromChannelOnEvent' }), - 500, - { - leading: true, - trailing: true, - }, - ); - - const setChannelUnreadUiState = useMemo( - () => - throttle(_setChannelUnreadUiState, 200, { - leading: true, - trailing: false, - }), - [], - ); + // const throttledCopyStateFromChannel = throttle( + // () => dispatch({ channel, type: 'copyStateFromChannelOnEvent' }), + // 500, + // { + // leading: true, + // trailing: true, + // }, + // ); + + // const setChannelUnreadUiState = useMemo( + // () => + // throttle(_setChannelUnreadUiState, 200, { + // leading: true, + // trailing: false, + // }), + // [], + // ); const markRead = useMemo( () => @@ -391,17 +391,18 @@ const ChannelInner = ( if (doMarkReadRequest) { doMarkReadRequest( channel, - updateChannelUiUnreadState ? setChannelUnreadUiState : undefined, + // updateChannelUiUnreadState ? setChannelUnreadUiState : undefined, ); } else { const markReadResponse = await channel.markRead(); // markReadResponse.event can be null in case of a user that is not a member of a channel being marked read // in that case event is null and we should not set unread UI if (updateChannelUiUnreadState && markReadResponse?.event) { - _setChannelUnreadUiState({ - last_read: lastRead.current, - last_read_message_id: markReadResponse.event.last_read_message_id, - unread_messages: 0, + channel.messagePaginator.unreadStateSnapshot.next({ + firstUnreadMessageId: null, + lastReadAt: lastRead.current, + lastReadMessageId: markReadResponse.event.last_read_message_id ?? null, + unreadCount: 0, }); } } @@ -423,18 +424,18 @@ const ChannelInner = ( channel, channelConfig, doMarkReadRequest, - setChannelUnreadUiState, + // setChannelUnreadUiState, t, ], ); const handleEvent = async (event: Event) => { if (event.message) { - dispatch({ - channel, - message: event.message, - type: 'updateThreadOnEvent', - }); + // dispatch({ + // channel, + // message: event.message, + // type: 'updateThreadOnEvent', + // }); } // ignore the event if it is not targeted at the current channel. @@ -445,9 +446,9 @@ const ChannelInner = ( if (event.type === 'user.watching.start' || event.type === 'user.watching.stop') return; - if (event.type === 'typing.start' || event.type === 'typing.stop') { - return dispatch({ channel, type: 'setTyping' }); - } + // if (event.type === 'typing.start' || event.type === 'typing.stop') { + // return dispatch({ channel, type: 'setTyping' }); + // } if (event.type === 'connection.changed' && typeof event.online === 'boolean') { online.current = event.online; @@ -503,22 +504,22 @@ const ChannelInner = ( }); } - if (event.type === 'notification.mark_unread') - _setChannelUnreadUiState((prev) => { - if (!(event.last_read_at && event.user)) return prev; - return { - first_unread_message_id: event.first_unread_message_id, - last_read: new Date(event.last_read_at), - last_read_message_id: event.last_read_message_id, - unread_messages: event.unread_messages ?? 0, - }; - }); - - if (event.type === 'channel.truncated' && event.cid === channel.cid) { - _setChannelUnreadUiState(undefined); - } - - throttledCopyStateFromChannel(); + // if (event.type === 'notification.mark_unread') + // _setChannelUnreadUiState((prev) => { + // if (!(event.last_read_at && event.user)) return prev; + // return { + // first_unread_message_id: event.first_unread_message_id, + // last_read: new Date(event.last_read_at), + // last_read_message_id: event.last_read_message_id, + // unread_messages: event.unread_messages ?? 0, + // }; + // }); + + // if (event.type === 'channel.truncated' && event.cid === channel.cid) { + // _setChannelUnreadUiState(undefined); + // } + + // throttledCopyStateFromChannel(); }; // useLayoutEffect here to prevent spinner. Use Suspense when it is available in stable release @@ -551,7 +552,7 @@ const ChannelInner = ( const config = channel.getConfig(); setChannelConfig(config); } catch (e) { - dispatch({ error: e as Error, type: 'setError' }); + // dispatch({ error: e as Error, type: 'setError' }); errored = true; } } @@ -560,17 +561,17 @@ const ChannelInner = ( originalTitle.current = document.title; if (!errored) { - dispatch({ - channel, - hasMore: channel.state.messagePagination.hasPrev, - type: 'initStateFromChannel', - }); - - if (client.user?.id && channel.state.read[client.user.id]) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { user, ...ownReadState } = channel.state.read[client.user.id]; - _setChannelUnreadUiState(ownReadState); - } + // dispatch({ + // channel, + // hasMore: channel.state.messagePagination.hasPrev, + // type: 'initStateFromChannel', + // }); + + // if (client.user?.id && channel.state.read[client.user.id]) { + // // eslint-disable-next-line @typescript-eslint/no-unused-vars + // const { user, ...ownReadState } = channel.state.read[client.user.id]; + // _setChannelUnreadUiState(ownReadState); + // } /** * TODO: maybe pass last_read to the countUnread method to get proper value * combined with channel.countUnread adjustment (_countMessageAsUnread) @@ -608,13 +609,13 @@ const ChannelInner = ( initializeOnMount, ]); - useEffect(() => { - if (!state.thread) return; - - const message = state.messages?.find((m) => m.id === state.thread?.id); - - if (message) dispatch({ message, type: 'setThread' }); - }, [state.messages, state.thread]); + // useEffect(() => { + // if (!state.thread) return; + // + // const message = state.messages?.find((m) => m.id === state.thread?.id); + // + // if (message) dispatch({ message, type: 'setThread' }); + // }, [state.messages, state.thread]); const handleHighlightedMessageChange = useCallback( ({ @@ -624,11 +625,11 @@ const ChannelInner = ( highlightedMessageId: string; highlightDuration?: number; }) => { - dispatch({ - channel, - highlightedMessageId, - type: 'jumpToMessageFinished', - }); + // dispatch({ + // channel, + // highlightedMessageId, + // type: 'jumpToMessageFinished', + // }); if (clearHighlightedMessageTimeoutId.current) { clearTimeout(clearHighlightedMessageTimeoutId.current); } @@ -637,10 +638,10 @@ const ChannelInner = ( searchController._internalState.partialNext({ focusedMessage: undefined }); } clearHighlightedMessageTimeoutId.current = null; - dispatch({ type: 'clearHighlightedMessage' }); + // dispatch({ type: 'clearHighlightedMessage' }); }, highlightDuration ?? DEFAULT_HIGHLIGHT_DURATION); }, - [channel, searchController], + [searchController._internalState], ); useEffect(() => { @@ -656,266 +657,265 @@ const ChannelInner = ( [], ); - // eslint-disable-next-line react-hooks/exhaustive-deps - const loadMoreFinished = useCallback( - debounce( - (hasMore: boolean, messages: ChannelState['messages']) => { - if (!isMounted.current) return; - dispatch({ hasMore, messages, type: 'loadMoreFinished' }); - }, - 2000, - { leading: true, trailing: true }, - ), - [], - ); - - const loadMore = async (limit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE) => { - if ( - !online.current || - !window.navigator.onLine || - !channel.state.messagePagination.hasPrev - ) - return 0; - - // prevent duplicate loading events... - const oldestMessage = state?.messages?.[0]; - - if ( - state.loadingMore || - state.loadingMoreNewer || - oldestMessage?.status !== 'received' - ) { - return 0; - } - - dispatch({ loadingMore: true, type: 'setLoadingMore' }); - - const oldestID = oldestMessage?.id; - const perPage = limit; - let queryResponse: ChannelAPIResponse; - - try { - queryResponse = await channel.query({ - messages: { id_lt: oldestID, limit: perPage }, - watchers: { limit: perPage }, - }); - } catch (e) { - console.warn('message pagination request failed with error', e); - dispatch({ loadingMore: false, type: 'setLoadingMore' }); - return 0; - } - - loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); - - return queryResponse.messages.length; - }; - - const loadMoreNewer = async (limit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE) => { - if ( - !online.current || - !window.navigator.onLine || - !channel.state.messagePagination.hasNext - ) - return 0; - - const newestMessage = state?.messages?.[state?.messages?.length - 1]; - if (state.loadingMore || state.loadingMoreNewer) return 0; - - dispatch({ loadingMoreNewer: true, type: 'setLoadingMoreNewer' }); - - const newestId = newestMessage?.id; - const perPage = limit; - let queryResponse: ChannelAPIResponse; - - try { - queryResponse = await channel.query({ - messages: { id_gt: newestId, limit: perPage }, - watchers: { limit: perPage }, - }); - } catch (e) { - console.warn('message pagination request failed with error', e); - dispatch({ loadingMoreNewer: false, type: 'setLoadingMoreNewer' }); - return 0; - } - - dispatch({ - hasMoreNewer: channel.state.messagePagination.hasNext, - messages: channel.state.messages, - type: 'loadMoreNewerFinished', - }); - return queryResponse.messages.length; - }; - - const jumpToMessage: ChannelActionContextValue['jumpToMessage'] = useCallback( - async ( - messageId, - messageLimit = DEFAULT_JUMP_TO_PAGE_SIZE, - highlightDuration = DEFAULT_HIGHLIGHT_DURATION, - ) => { - dispatch({ loadingMore: true, type: 'setLoadingMore' }); - await channel.state.loadMessageIntoState(messageId, undefined, messageLimit); - - loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); - handleHighlightedMessageChange({ - highlightDuration, - highlightedMessageId: messageId, - }); - }, - [channel, handleHighlightedMessageChange, loadMoreFinished], - ); - - const jumpToLatestMessage: ChannelActionContextValue['jumpToLatestMessage'] = - useCallback(async () => { - await channel.state.loadMessageIntoState('latest'); - loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); - dispatch({ - type: 'jumpToLatestMessage', - }); - }, [channel, loadMoreFinished]); - - const jumpToFirstUnreadMessage: ChannelActionContextValue['jumpToFirstUnreadMessage'] = - useCallback( - async ( - queryMessageLimit = DEFAULT_JUMP_TO_PAGE_SIZE, - highlightDuration = DEFAULT_HIGHLIGHT_DURATION, - ) => { - if (!channelUnreadUiState?.unread_messages) return; - let lastReadMessageId = channelUnreadUiState?.last_read_message_id; - let firstUnreadMessageId = channelUnreadUiState?.first_unread_message_id; - let isInCurrentMessageSet = false; - - if (firstUnreadMessageId) { - const result = findInMsgSetById(firstUnreadMessageId, channel.state.messages); - isInCurrentMessageSet = result.index !== -1; - } else if (lastReadMessageId) { - const result = findInMsgSetById(lastReadMessageId, channel.state.messages); - isInCurrentMessageSet = !!result.target; - firstUnreadMessageId = - result.index > -1 ? channel.state.messages[result.index + 1]?.id : undefined; - } else { - const lastReadTimestamp = channelUnreadUiState.last_read.getTime(); - const { index: lastReadMessageIndex, target: lastReadMessage } = - findInMsgSetByDate( - channelUnreadUiState.last_read, - channel.state.messages, - true, - ); - - if (lastReadMessage) { - firstUnreadMessageId = channel.state.messages[lastReadMessageIndex + 1]?.id; - isInCurrentMessageSet = !!firstUnreadMessageId; - lastReadMessageId = lastReadMessage.id; - } else { - dispatch({ loadingMore: true, type: 'setLoadingMore' }); - let messages; - try { - messages = ( - await channel.query( - { - messages: { - created_at_around: channelUnreadUiState.last_read.toISOString(), - limit: queryMessageLimit, - }, - }, - 'new', - ) - ).messages; - } catch (e) { - addNotification(t('Failed to jump to the first unread message'), 'error'); - loadMoreFinished( - channel.state.messagePagination.hasPrev, - channel.state.messages, - ); - return; - } - - const firstMessageWithCreationDate = messages.find((msg) => msg.created_at); - if (!firstMessageWithCreationDate) { - addNotification(t('Failed to jump to the first unread message'), 'error'); - loadMoreFinished( - channel.state.messagePagination.hasPrev, - channel.state.messages, - ); - return; - } - const firstMessageTimestamp = new Date( - firstMessageWithCreationDate.created_at as string, - ).getTime(); - if (lastReadTimestamp < firstMessageTimestamp) { - // whole channel is unread - firstUnreadMessageId = firstMessageWithCreationDate.id; - } else { - const result = findInMsgSetByDate(channelUnreadUiState.last_read, messages); - lastReadMessageId = result.target?.id; - } - loadMoreFinished( - channel.state.messagePagination.hasPrev, - channel.state.messages, - ); - } - } - - if (!firstUnreadMessageId && !lastReadMessageId) { - addNotification(t('Failed to jump to the first unread message'), 'error'); - return; - } - - if (!isInCurrentMessageSet) { - dispatch({ loadingMore: true, type: 'setLoadingMore' }); - try { - const targetId = (firstUnreadMessageId ?? lastReadMessageId) as string; - await channel.state.loadMessageIntoState( - targetId, - undefined, - queryMessageLimit, - ); - /** - * if the index of the last read message on the page is beyond the half of the page, - * we have arrived to the oldest page of the channel - */ - const indexOfTarget = channel.state.messages.findIndex( - (message) => message.id === targetId, - ) as number; - loadMoreFinished( - channel.state.messagePagination.hasPrev, - channel.state.messages, - ); - firstUnreadMessageId = - firstUnreadMessageId ?? channel.state.messages[indexOfTarget + 1]?.id; - } catch (e) { - addNotification(t('Failed to jump to the first unread message'), 'error'); - loadMoreFinished( - channel.state.messagePagination.hasPrev, - channel.state.messages, - ); - return; - } - } - - if (!firstUnreadMessageId) { - addNotification(t('Failed to jump to the first unread message'), 'error'); - return; - } - if (!channelUnreadUiState.first_unread_message_id) - _setChannelUnreadUiState({ - ...channelUnreadUiState, - first_unread_message_id: firstUnreadMessageId, - last_read_message_id: lastReadMessageId, - }); - handleHighlightedMessageChange({ - highlightDuration, - highlightedMessageId: firstUnreadMessageId, - }); - }, - [ - addNotification, - channel, - handleHighlightedMessageChange, - loadMoreFinished, - t, - channelUnreadUiState, - ], - ); + // const loadMoreFinished = useCallback( + // debounce( + // (hasMore: boolean, messages: ChannelState['messages']) => { + // if (!isMounted.current) return; + // dispatch({ hasMore, messages, type: 'loadMoreFinished' }); + // }, + // 2000, + // { leading: true, trailing: true }, + // ), + // [], + // ); + + // const loadMore = async (limit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE) => { + // if ( + // !online.current || + // !window.navigator.onLine || + // !channel.state.messagePagination.hasPrev + // ) + // return 0; + // + // // prevent duplicate loading events... + // const oldestMessage = state?.messages?.[0]; + // + // if ( + // state.loadingMore || + // state.loadingMoreNewer || + // oldestMessage?.status !== 'received' + // ) { + // return 0; + // } + // + // dispatch({ loadingMore: true, type: 'setLoadingMore' }); + // + // const oldestID = oldestMessage?.id; + // const perPage = limit; + // let queryResponse: ChannelAPIResponse; + // + // try { + // queryResponse = await channel.query({ + // messages: { id_lt: oldestID, limit: perPage }, + // watchers: { limit: perPage }, + // }); + // } catch (e) { + // console.warn('message pagination request failed with error', e); + // dispatch({ loadingMore: false, type: 'setLoadingMore' }); + // return 0; + // } + // + // loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); + // + // return queryResponse.messages.length; + // }; + + // const loadMoreNewer = async (limit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE) => { + // if ( + // !online.current || + // !window.navigator.onLine || + // !channel.state.messagePagination.hasNext + // ) + // return 0; + // + // const newestMessage = state?.messages?.[state?.messages?.length - 1]; + // if (state.loadingMore || state.loadingMoreNewer) return 0; + // + // dispatch({ loadingMoreNewer: true, type: 'setLoadingMoreNewer' }); + // + // const newestId = newestMessage?.id; + // const perPage = limit; + // let queryResponse: ChannelAPIResponse; + // + // try { + // queryResponse = await channel.query({ + // messages: { id_gt: newestId, limit: perPage }, + // watchers: { limit: perPage }, + // }); + // } catch (e) { + // console.warn('message pagination request failed with error', e); + // dispatch({ loadingMoreNewer: false, type: 'setLoadingMoreNewer' }); + // return 0; + // } + // + // dispatch({ + // hasMoreNewer: channel.state.messagePagination.hasNext, + // messages: channel.state.messages, + // type: 'loadMoreNewerFinished', + // }); + // return queryResponse.messages.length; + // }; + + // const jumpToMessage: ChannelActionContextValue['jumpToMessage'] = useCallback( + // async ( + // messageId, + // messageLimit = DEFAULT_JUMP_TO_PAGE_SIZE, + // highlightDuration = DEFAULT_HIGHLIGHT_DURATION, + // ) => { + // dispatch({ loadingMore: true, type: 'setLoadingMore' }); + // await channel.state.loadMessageIntoState(messageId, undefined, messageLimit); + // + // loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); + // handleHighlightedMessageChange({ + // highlightDuration, + // highlightedMessageId: messageId, + // }); + // }, + // [channel, handleHighlightedMessageChange, loadMoreFinished], + // ); + + // const jumpToLatestMessage: ChannelActionContextValue['jumpToLatestMessage'] = + // useCallback(async () => { + // await channel.state.loadMessageIntoState('latest'); + // loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages); + // dispatch({ + // type: 'jumpToLatestMessage', + // }); + // }, [channel, loadMoreFinished]); + // + // const jumpToFirstUnreadMessage: ChannelActionContextValue['jumpToFirstUnreadMessage'] = + // useCallback( + // async ( + // queryMessageLimit = DEFAULT_JUMP_TO_PAGE_SIZE, + // highlightDuration = DEFAULT_HIGHLIGHT_DURATION, + // ) => { + // if (!channelUnreadUiState?.unread_messages) return; + // let lastReadMessageId = channelUnreadUiState?.last_read_message_id; + // let firstUnreadMessageId = channelUnreadUiState?.first_unread_message_id; + // let isInCurrentMessageSet = false; + // + // if (firstUnreadMessageId) { + // const result = findInMsgSetById(firstUnreadMessageId, channel.state.messages); + // isInCurrentMessageSet = result.index !== -1; + // } else if (lastReadMessageId) { + // const result = findInMsgSetById(lastReadMessageId, channel.state.messages); + // isInCurrentMessageSet = !!result.target; + // firstUnreadMessageId = + // result.index > -1 ? channel.state.messages[result.index + 1]?.id : undefined; + // } else { + // const lastReadTimestamp = channelUnreadUiState.last_read.getTime(); + // const { index: lastReadMessageIndex, target: lastReadMessage } = + // findInMsgSetByDate( + // channelUnreadUiState.last_read, + // channel.state.messages, + // true, + // ); + // + // if (lastReadMessage) { + // firstUnreadMessageId = channel.state.messages[lastReadMessageIndex + 1]?.id; + // isInCurrentMessageSet = !!firstUnreadMessageId; + // lastReadMessageId = lastReadMessage.id; + // } else { + // dispatch({ loadingMore: true, type: 'setLoadingMore' }); + // let messages; + // try { + // messages = ( + // await channel.query( + // { + // messages: { + // created_at_around: channelUnreadUiState.last_read.toISOString(), + // limit: queryMessageLimit, + // }, + // }, + // 'new', + // ) + // ).messages; + // } catch (e) { + // addNotification(t('Failed to jump to the first unread message'), 'error'); + // loadMoreFinished( + // channel.state.messagePagination.hasPrev, + // channel.state.messages, + // ); + // return; + // } + // + // const firstMessageWithCreationDate = messages.find((msg) => msg.created_at); + // if (!firstMessageWithCreationDate) { + // addNotification(t('Failed to jump to the first unread message'), 'error'); + // loadMoreFinished( + // channel.state.messagePagination.hasPrev, + // channel.state.messages, + // ); + // return; + // } + // const firstMessageTimestamp = new Date( + // firstMessageWithCreationDate.created_at as string, + // ).getTime(); + // if (lastReadTimestamp < firstMessageTimestamp) { + // // whole channel is unread + // firstUnreadMessageId = firstMessageWithCreationDate.id; + // } else { + // const result = findInMsgSetByDate(channelUnreadUiState.last_read, messages); + // lastReadMessageId = result.target?.id; + // } + // loadMoreFinished( + // channel.state.messagePagination.hasPrev, + // channel.state.messages, + // ); + // } + // } + // + // if (!firstUnreadMessageId && !lastReadMessageId) { + // addNotification(t('Failed to jump to the first unread message'), 'error'); + // return; + // } + // + // if (!isInCurrentMessageSet) { + // dispatch({ loadingMore: true, type: 'setLoadingMore' }); + // try { + // const targetId = (firstUnreadMessageId ?? lastReadMessageId) as string; + // await channel.state.loadMessageIntoState( + // targetId, + // undefined, + // queryMessageLimit, + // ); + // /** + // * if the index of the last read message on the page is beyond the half of the page, + // * we have arrived to the oldest page of the channel + // */ + // const indexOfTarget = channel.state.messages.findIndex( + // (message) => message.id === targetId, + // ) as number; + // loadMoreFinished( + // channel.state.messagePagination.hasPrev, + // channel.state.messages, + // ); + // firstUnreadMessageId = + // firstUnreadMessageId ?? channel.state.messages[indexOfTarget + 1]?.id; + // } catch (e) { + // addNotification(t('Failed to jump to the first unread message'), 'error'); + // loadMoreFinished( + // channel.state.messagePagination.hasPrev, + // channel.state.messages, + // ); + // return; + // } + // } + // + // if (!firstUnreadMessageId) { + // addNotification(t('Failed to jump to the first unread message'), 'error'); + // return; + // } + // if (!channelUnreadUiState.first_unread_message_id) + // _setChannelUnreadUiState({ + // ...channelUnreadUiState, + // first_unread_message_id: firstUnreadMessageId, + // last_read_message_id: lastReadMessageId, + // }); + // handleHighlightedMessageChange({ + // highlightDuration, + // highlightedMessageId: firstUnreadMessageId, + // }); + // }, + // [ + // addNotification, + // channel, + // handleHighlightedMessageChange, + // loadMoreFinished, + // t, + // channelUnreadUiState, + // ], + // ); const deleteMessage = useCallback( async ( @@ -942,218 +942,217 @@ const ChannelInner = ( // add the message to the local channel state channel.state.addMessageSorted(updatedMessage, true); - dispatch({ - channel, - parentId: state.thread && updatedMessage.parent_id, - type: 'copyMessagesFromChannel', - }); + // dispatch({ + // channel, + // parentId: state.thread && updatedMessage.parent_id, + // type: 'copyMessagesFromChannel', + // }); }; - const doSendMessage = async ({ - localMessage, - message, - options, - }: { - localMessage: LocalMessage; - message: Message; - options?: SendMessageOptions; - }) => { - try { - let messageResponse: void | SendMessageAPIResponse; - - if (doSendMessageRequest) { - messageResponse = await doSendMessageRequest(channel, message, options); - } else { - messageResponse = await channel.sendMessage(message, options); - } - - let existingMessage: LocalMessage | undefined = undefined; - for (let i = channel.state.messages.length - 1; i >= 0; i--) { - const msg = channel.state.messages[i]; - if (msg.id && msg.id === message.id) { - existingMessage = msg; - break; - } - } - - const responseTimestamp = new Date( - messageResponse?.message?.updated_at || 0, - ).getTime(); - const existingMessageTimestamp = existingMessage?.updated_at?.getTime() || 0; - const responseIsTheNewest = responseTimestamp > existingMessageTimestamp; - - // Replace the message payload after send is completed - // We need to check for the newest message payload, because on slow network, the response can arrive later than WS events message.new, message.updated. - // Always override existing message in status "sending" - if ( - messageResponse?.message && - (responseIsTheNewest || existingMessage?.status === 'sending') - ) { - updateMessage({ - ...messageResponse.message, - status: 'received', - }); - } - } catch (error) { - // error response isn't usable so needs to be stringified then parsed - const stringError = JSON.stringify(error); - const parsedError = ( - stringError ? JSON.parse(stringError) : {} - ) as ErrorFromResponse; - - // Handle the case where the message already exists - // (typically, when retrying to send a message). - // If the message already exists, we can assume it was sent successfully, - // so we update the message status to "received". - // Right now, the only way to check this error is by checking - // the combination of the error code and the error description, - // since there is no special error code for duplicate messages. - if ( - parsedError.code === 4 && - error instanceof Error && - error.message.includes('already exists') - ) { - updateMessage({ - ...localMessage, - status: 'received', - }); - } else { - updateMessage({ - ...localMessage, - error: parsedError, - status: 'failed', - }); - - thread?.upsertReplyLocally({ - message: { - ...localMessage, - error: parsedError, - status: 'failed', - }, - }); - } - } - }; - - const sendMessage = async ({ - localMessage, - message, - options, - }: { - localMessage: LocalMessage; - message: Message; - options?: SendMessageOptions; - }) => { - channel.state.filterErrorMessages(); - - thread?.upsertReplyLocally({ - message: localMessage, - }); - - updateMessage(localMessage); - - await doSendMessage({ localMessage, message, options }); - }; - - const retrySendMessage = async (localMessage: LocalMessage) => { - updateMessage({ - ...localMessage, - error: undefined, - status: 'sending', - }); - - await doSendMessage({ - localMessage, - message: localMessageToNewMessagePayload(localMessage), - }); - }; + // const doSendMessage = async ({ + // localMessage, + // message, + // options, + // }: { + // localMessage: LocalMessage; + // message: Message; + // options?: SendMessageOptions; + // }) => { + // try { + // let messageResponse: void | SendMessageAPIResponse; + // + // if (doSendMessageRequest) { + // messageResponse = await doSendMessageRequest(channel, message, options); + // } else { + // messageResponse = await channel.sendMessage(message, options); + // } + // + // let existingMessage: LocalMessage | undefined = undefined; + // for (let i = channel.state.messages.length - 1; i >= 0; i--) { + // const msg = channel.state.messages[i]; + // if (msg.id && msg.id === message.id) { + // existingMessage = msg; + // break; + // } + // } + // + // const responseTimestamp = new Date( + // messageResponse?.message?.updated_at || 0, + // ).getTime(); + // const existingMessageTimestamp = existingMessage?.updated_at?.getTime() || 0; + // const responseIsTheNewest = responseTimestamp > existingMessageTimestamp; + // + // // Replace the message payload after send is completed + // // We need to check for the newest message payload, because on slow network, the response can arrive later than WS events message.new, message.updated. + // // Always override existing message in status "sending" + // if ( + // messageResponse?.message && + // (responseIsTheNewest || existingMessage?.status === 'sending') + // ) { + // updateMessage({ + // ...messageResponse.message, + // status: 'received', + // }); + // } + // } catch (error) { + // // error response isn't usable so needs to be stringified then parsed + // const stringError = JSON.stringify(error); + // const parsedError = ( + // stringError ? JSON.parse(stringError) : {} + // ) as ErrorFromResponse; + // + // // Handle the case where the message already exists + // // (typically, when retrying to send a message). + // // If the message already exists, we can assume it was sent successfully, + // // so we update the message status to "received". + // // Right now, the only way to check this error is by checking + // // the combination of the error code and the error description, + // // since there is no special error code for duplicate messages. + // if ( + // parsedError.code === 4 && + // error instanceof Error && + // error.message.includes('already exists') + // ) { + // updateMessage({ + // ...localMessage, + // status: 'received', + // }); + // } else { + // updateMessage({ + // ...localMessage, + // error: parsedError, + // status: 'failed', + // }); + // + // thread?.upsertReplyLocally({ + // message: { + // ...localMessage, + // error: parsedError, + // status: 'failed', + // }, + // }); + // } + // } + // }; + + // const sendMessage = async ({ + // localMessage, + // message, + // options, + // }: { + // localMessage: LocalMessage; + // message: Message; + // options?: SendMessageOptions; + // }) => { + // channel.state.filterErrorMessages(); + // + // thread?.upsertReplyLocally({ + // message: localMessage, + // }); + // + // updateMessage(localMessage); + // + // await doSendMessage({ localMessage, message, options }); + // }; + + // const retrySendMessage = async (localMessage: LocalMessage) => { + // updateMessage({ + // ...localMessage, + // error: undefined, + // status: 'sending', + // }); + // + // await doSendMessage({ + // localMessage, + // message: localMessageToNewMessagePayload(localMessage), + // }); + // }; const removeMessage = (message: LocalMessage) => { channel.state.removeMessage(message); - dispatch({ - channel, - parentId: state.thread && message.parent_id, - type: 'copyMessagesFromChannel', - }); + // dispatch({ + // channel, + // parentId: state.thread && message.parent_id, + // type: 'copyMessagesFromChannel', + // }); }; /** THREAD */ const openThread = (message: LocalMessage, event?: React.BaseSyntheticEvent) => { event?.preventDefault(); - dispatch({ channel, message, type: 'openThread' }); + // dispatch({ channel, message, type: 'openThread' }); }; const closeThread = (event?: React.BaseSyntheticEvent) => { event?.preventDefault(); - dispatch({ type: 'closeThread' }); + // dispatch({ type: 'closeThread' }); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - const loadMoreThreadFinished = useCallback( - debounce( - ( - threadHasMore: boolean, - threadMessages: Array>, - ) => { - dispatch({ - threadHasMore, - threadMessages, - type: 'loadMoreThreadFinished', - }); - }, - 2000, - { leading: true, trailing: true }, - ), - [], - ); - - const loadMoreThread = async (limit: number = DEFAULT_THREAD_PAGE_SIZE) => { - // FIXME: should prevent loading more, if state.thread.reply_count === channel.state.threads[parentID].length - if (state.threadLoadingMore || !state.thread || !state.threadHasMore) return; - - dispatch({ type: 'startLoadingThread' }); - const parentId = state.thread.id; - - if (!parentId) { - return dispatch({ type: 'closeThread' }); - } - - const oldMessages = channel.state.threads[parentId] || []; - const oldestMessageId = oldMessages[0]?.id; - - try { - const queryResponse = await channel.getReplies(parentId, { - id_lt: oldestMessageId, - limit, - }); - - const threadHasMoreMessages = hasMoreMessagesProbably( - queryResponse.messages.length, - limit, - ); - const newThreadMessages = channel.state.threads[parentId] || []; - - // next set loadingMore to false so we can start asking for more data - loadMoreThreadFinished(threadHasMoreMessages, newThreadMessages); - } catch (e) { - loadMoreThreadFinished(false, oldMessages); - } - }; + // const loadMoreThreadFinished = useCallback( + // debounce( + // ( + // threadHasMore: boolean, + // threadMessages: Array>, + // ) => { + // dispatch({ + // threadHasMore, + // threadMessages, + // type: 'loadMoreThreadFinished', + // }); + // }, + // 2000, + // { leading: true, trailing: true }, + // ), + // [], + // ); + + // const loadMoreThread = async (limit: number = DEFAULT_THREAD_PAGE_SIZE) => { + // // FIXME: should prevent loading more, if state.thread.reply_count === channel.state.threads[parentID].length + // if (state.threadLoadingMore || !state.thread || !state.threadHasMore) return; + // + // dispatch({ type: 'startLoadingThread' }); + // const parentId = state.thread.id; + // + // if (!parentId) { + // return dispatch({ type: 'closeThread' }); + // } + // + // const oldMessages = channel.state.threads[parentId] || []; + // const oldestMessageId = oldMessages[0]?.id; + // + // try { + // const queryResponse = await channel.getReplies(parentId, { + // id_lt: oldestMessageId, + // limit, + // }); + // + // const threadHasMoreMessages = hasMoreMessagesProbably( + // queryResponse.messages.length, + // limit, + // ); + // const newThreadMessages = channel.state.threads[parentId] || []; + // + // // next set loadingMore to false so we can start asking for more data + // loadMoreThreadFinished(threadHasMoreMessages, newThreadMessages); + // } catch (e) { + // loadMoreThreadFinished(false, oldMessages); + // } + // }; const onMentionsHoverOrClick = useMentionsHandlers(onMentionsHover, onMentionsClick); - const editMessage = useEditMessageHandler(doUpdateMessageRequest); + // const editMessage = useEditMessageHandler(doUpdateMessageRequest); - const { typing, ...restState } = state; + // const { typing, ...restState } = state; const channelStateContextValue = useCreateChannelStateContext({ - ...restState, + // ...restState, channel, channelCapabilitiesArray, channelConfig, - channelUnreadUiState, + // channelUnreadUiState, giphyVersion: props.giphyVersion || 'fixed_height', imageAttachmentSizeHandler: props.imageAttachmentSizeHandler || getImageAttachmentConfiguration, @@ -1162,7 +1161,7 @@ const ChannelInner = ( shouldGenerateVideoThumbnail: props.shouldGenerateVideoThumbnail || true, videoAttachmentSizeHandler: props.videoAttachmentSizeHandler || getVideoAttachmentConfiguration, - watcher_count: state.watcherCount, + // watcher_count: state.watcherCount, }); const channelActionContextValue: ChannelActionContextValue = useMemo( @@ -1170,22 +1169,22 @@ const ChannelInner = ( addNotification, closeThread, deleteMessage, - dispatch, - editMessage, - jumpToFirstUnreadMessage, - jumpToLatestMessage, - jumpToMessage, - loadMore, - loadMoreNewer, - loadMoreThread, + // dispatch, + // editMessage, + // jumpToFirstUnreadMessage, + // jumpToLatestMessage, + // jumpToMessage, + // loadMore, + // loadMoreNewer, + // loadMoreThread, markRead, onMentionsClick: onMentionsHoverOrClick, onMentionsHover: onMentionsHoverOrClick, openThread, removeMessage, - retrySendMessage, - sendMessage, - setChannelUnreadUiState, + // retrySendMessage, + // sendMessage, + // setChannelUnreadUiState, skipMessageDataMemoization, updateMessage, }), @@ -1193,13 +1192,13 @@ const ChannelInner = ( [ channel.cid, deleteMessage, - loadMore, - loadMoreNewer, + // loadMore, + // loadMoreNewer, markRead, - jumpToFirstUnreadMessage, - jumpToMessage, - jumpToLatestMessage, - setChannelUnreadUiState, + // jumpToFirstUnreadMessage, + // jumpToMessage, + // jumpToLatestMessage, + // setChannelUnreadUiState, ], ); @@ -1346,25 +1345,25 @@ const ChannelInner = ( ], ); - const typingContextValue = useCreateTypingContext({ - typing, - }); - - if (state.error) { - return ( - - - - ); - } - - if (state.loading) { - return ( - - - - ); - } + // const typingContextValue = useCreateTypingContext({ + // typing, + // }); + + // if (state.error) { + // return ( + // + // + // + // ); + // } + + // if (state.loading) { + // return ( + // + // + // + // ); + // } if (!channel.watch) { return ( @@ -1379,11 +1378,11 @@ const ChannelInner = ( - - -
{children}
-
-
+ {/**/} + +
{children}
+
+ {/*
*/}
diff --git a/src/components/Channel/channelState.ts b/src/components/Channel/channelState.ts index 597e1ca36b..2bccac8310 100644 --- a/src/components/Channel/channelState.ts +++ b/src/components/Channel/channelState.ts @@ -272,17 +272,24 @@ export const initialState = { hasMoreNewer: false, loading: true, loadingMore: false, + // todo: add reactive state to Channel class members: {}, messages: [], + // todo: add reactive state to Channel class pinnedMessages: [], + // todo: add reactive state to Channel class read: {}, + // todo: could be moved as a prop to MessageList / VML suppressAutoscroll: false, thread: null, threadHasMore: true, threadLoadingMore: false, threadMessages: [], + // todo: could be moved as a prop to MessageList / VML threadSuppressAutoscroll: false, + // todo: add reactive state to Channel class typing: {}, + // todo: add reactive state to Channel class watcherCount: 0, watchers: {}, }; diff --git a/src/components/Channel/hooks/useCreateChannelStateContext.ts b/src/components/Channel/hooks/useCreateChannelStateContext.ts index 3986592791..e08b316976 100644 --- a/src/components/Channel/hooks/useCreateChannelStateContext.ts +++ b/src/components/Channel/hooks/useCreateChannelStateContext.ts @@ -14,49 +14,49 @@ export const useCreateChannelStateContext = ( channel, channelCapabilitiesArray = [], channelConfig, - channelUnreadUiState, - error, + // channelUnreadUiState, + // error, giphyVersion, - hasMore, - hasMoreNewer, - highlightedMessageId, + // hasMore, + // hasMoreNewer, + // highlightedMessageId, imageAttachmentSizeHandler, - loading, - loadingMore, - members, - messages = [], + // loading, + // loadingMore, + // members, + // messages = [], mutes, notifications, - pinnedMessages, - read = {}, + // pinnedMessages, + // read = {}, shouldGenerateVideoThumbnail, - skipMessageDataMemoization, - suppressAutoscroll, - thread, - threadHasMore, - threadLoadingMore, - threadMessages = [], + // skipMessageDataMemoization, + // suppressAutoscroll, + // thread, + // threadHasMore, + // threadLoadingMore, + // threadMessages = [], videoAttachmentSizeHandler, - watcher_count, - watcherCount, - watchers, + // watcher_count, + // watcherCount, + // watchers, } = value; - const channelId = channel.cid; - const lastRead = channel.initialized && channel.lastRead()?.getTime(); - const membersLength = Object.keys(members || []).length; - const notificationsLength = notifications.length; - const readUsers = Object.values(read); - const readUsersLength = readUsers.length; - const readUsersLastReadDateStrings: string[] = []; - for (const { last_read } of readUsers) { - if (!lastRead) continue; - readUsersLastReadDateStrings.push(last_read?.toISOString()); - } - const readUsersLastReads = readUsersLastReadDateStrings.join(); - const threadMessagesLength = threadMessages?.length; + // const channelId = channel.cid; + // const lastRead = channel.initialized && channel.lastRead()?.getTime(); + // const membersLength = Object.keys(members || []).length; + // const notificationsLength = notifications.length; + // const readUsers = Object.values(read); + // const readUsersLength = readUsers.length; + // const readUsersLastReadDateStrings: string[] = []; + // for (const { last_read } of readUsers) { + // if (!lastRead) continue; + // readUsersLastReadDateStrings.push(last_read?.toISOString()); + // } + // const readUsersLastReads = readUsersLastReadDateStrings.join(); + // const threadMessagesLength = threadMessages?.length; - const channelCapabilities: Record = {}; + const channelCapabilities: Record = useMemo(() => ({}), []); channelCapabilitiesArray.forEach((capability) => { channelCapabilities[capability] = true; @@ -64,100 +64,107 @@ export const useCreateChannelStateContext = ( // FIXME: this is crazy - I could not find out why the messages were not getting updated when only message properties that are not part // of this serialization has been changed. A great example of memoization gone wrong. - const memoizedMessageData = skipMessageDataMemoization - ? messages - : messages - .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)) - ? updated_at.toISOString() - : updated_at || '' - }${user?.updated_at}`, - ) - .join(); + // const memoizedMessageData = skipMessageDataMemoization + // ? messages + // : messages + // .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)) + // ? updated_at.toISOString() + // : updated_at || '' + // }${user?.updated_at}`, + // ) + // .join(); - const memoizedThreadMessageData = threadMessages - .map( - ({ deleted_at, latest_reactions, pinned, status, updated_at, user }) => - `${deleted_at}${ - latest_reactions ? latest_reactions.map(({ type }) => type).join() : '' - }${pinned}${status}${ - updated_at && (isDayOrMoment(updated_at) || isDate(updated_at)) - ? updated_at.toISOString() - : updated_at || '' - }${user?.updated_at}`, - ) - .join(); + // const memoizedThreadMessageData = threadMessages + // .map( + // ({ deleted_at, latest_reactions, pinned, status, updated_at, user }) => + // `${deleted_at}${ + // latest_reactions ? latest_reactions.map(({ type }) => type).join() : '' + // }${pinned}${status}${ + // updated_at && (isDayOrMoment(updated_at) || isDate(updated_at)) + // ? updated_at.toISOString() + // : updated_at || '' + // }${user?.updated_at}`, + // ) + // .join(); const channelStateContext: ChannelStateContextValue = useMemo( () => ({ channel, channelCapabilities, channelConfig, - channelUnreadUiState, - error, + // channelUnreadUiState, + // error, giphyVersion, - hasMore, - hasMoreNewer, - highlightedMessageId, + // hasMore, + // hasMoreNewer, + // highlightedMessageId, imageAttachmentSizeHandler, - loading, - loadingMore, - members, - messages, + // loading, + // loadingMore, + // members, + // messages, mutes, notifications, - pinnedMessages, - read, + // pinnedMessages, + // read, shouldGenerateVideoThumbnail, - suppressAutoscroll, - thread, - threadHasMore, - threadLoadingMore, - threadMessages, + // suppressAutoscroll, + // thread, + // threadHasMore, + // threadLoadingMore, + // threadMessages, videoAttachmentSizeHandler, - watcher_count, - watcherCount, - watchers, + // watcher_count, + // watcherCount, + // watchers, }), - // eslint-disable-next-line react-hooks/exhaustive-deps [ - channel.data?.name, // otherwise ChannelHeader will not be updated - channelId, - channelUnreadUiState, - error, - hasMore, - hasMoreNewer, - highlightedMessageId, - lastRead, - loading, - loadingMore, - membersLength, - memoizedMessageData, - memoizedThreadMessageData, - notificationsLength, - readUsersLength, - readUsersLastReads, + channel, + // channel.data?.name, // otherwise ChannelHeader will not be updated + channelCapabilities, + channelConfig, + // channelId, + // channelUnreadUiState, + giphyVersion, + // error, + // hasMore, + // hasMoreNewer, + // highlightedMessageId, + imageAttachmentSizeHandler, + // lastRead, + // loading, + // loadingMore, + // membersLength, + // memoizedMessageData, + // memoizedThreadMessageData, + mutes, + notifications, + // notificationsLength, + // readUsersLength, + // readUsersLastReads, shouldGenerateVideoThumbnail, - skipMessageDataMemoization, - suppressAutoscroll, - thread, - threadHasMore, - threadLoadingMore, - threadMessagesLength, - watcherCount, + // skipMessageDataMemoization, + // suppressAutoscroll, + // thread, + // threadHasMore, + // threadLoadingMore, + // threadMessagesLength, + videoAttachmentSizeHandler, + // watcherCount, ], ); diff --git a/src/components/ChannelHeader/ChannelHeader.tsx b/src/components/ChannelHeader/ChannelHeader.tsx index 802478352e..ceb829aebf 100644 --- a/src/components/ChannelHeader/ChannelHeader.tsx +++ b/src/components/ChannelHeader/ChannelHeader.tsx @@ -33,7 +33,7 @@ export const ChannelHeader = (props: ChannelHeaderProps) => { title: overrideTitle, } = props; - const { channel, watcher_count } = useChannelStateContext('ChannelHeader'); + const { channel } = useChannelStateContext('ChannelHeader'); const { openMobileNav } = useChatContext('ChannelHeader'); const { t } = useTranslationContext('ChannelHeader'); const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({ @@ -76,7 +76,8 @@ export const ChannelHeader = (props: ChannelHeaderProps) => { ,{' '} )} - {t('{{ watcherCount }} online', { watcherCount: watcher_count })} + {/*todo: get the watcher count from LLC reactive state */} + {/*{t('{{ watcherCount }} online', { watcherCount: watcher_count })}*/}

diff --git a/src/components/ChatView/ChatView.tsx b/src/components/ChatView/ChatView.tsx index 014731c87c..e080c30d17 100644 --- a/src/components/ChatView/ChatView.tsx +++ b/src/components/ChatView/ChatView.tsx @@ -10,7 +10,7 @@ import type { PropsWithChildren } from 'react'; import type { Thread, ThreadManagerState } from 'stream-chat'; import clsx from 'clsx'; -type ChatView = 'channels' | 'threads'; +type ChatView = 'channels' | 'threads' | (string & {}); type ChatViewContextValue = { activeChatView: ChatView; @@ -37,6 +37,7 @@ export const ChatView = ({ children }: PropsWithChildren) => { ); }; +// todo: move channel list orchestrator here const ChannelsView = ({ children }: PropsWithChildren) => { const { activeChatView } = useContext(ChatViewContext); diff --git a/src/components/InfiniteScrollPaginator/InfiniteScrollPaginator.tsx b/src/components/InfiniteScrollPaginator/InfiniteScrollPaginator.tsx index 7457a1b69c..4118d5469f 100644 --- a/src/components/InfiniteScrollPaginator/InfiniteScrollPaginator.tsx +++ b/src/components/InfiniteScrollPaginator/InfiniteScrollPaginator.tsx @@ -1,6 +1,7 @@ import clsx from 'clsx'; import debounce from 'lodash.debounce'; import type { PropsWithChildren } from 'react'; +import { forwardRef } from 'react'; import React, { useEffect, useMemo, useRef } from 'react'; import { DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD } from '../../constants/limits'; @@ -14,7 +15,8 @@ const mousewheelListener = (event: Event) => { } }; -export type InfiniteScrollPaginatorProps = React.ComponentProps<'div'> & { +type InfiniteScrollPaginatorOwnProps = { + element?: React.ElementType; listenToScroll?: ( distanceFromBottom: number, distanceFromTop: number, @@ -28,12 +30,39 @@ export type InfiniteScrollPaginatorProps = React.ComponentProps<'div'> & { useCapture?: boolean; }; -export const InfiniteScrollPaginator = ( - props: PropsWithChildren, -) => { +// helper: get the right ref type for a given element/component +type PolymorphicRef = React.ComponentPropsWithRef['ref']; + +// polymorphic props, defaulting to 'div' +export type InfiniteScrollPaginatorProps = + PropsWithChildren< + InfiniteScrollPaginatorOwnProps & { + element?: C; + } & Omit< + React.ComponentPropsWithRef, + keyof InfiniteScrollPaginatorOwnProps | 'element' + > + >; + +type InfiniteScrollPaginatorComponent = ( + props: InfiniteScrollPaginatorProps & { + ref?: PolymorphicRef; + }, +) => React.ReactNode; + +const renderPolymorphic = ( + Comp: C, + props: React.ComponentPropsWithRef & { ref?: PolymorphicRef }, + children?: React.ReactNode, +) => React.createElement(Comp, props, children); + +export const InfiniteScrollPaginator = forwardRef(function InfiniteScrollPaginator< + E extends React.ElementType = 'div', +>(props: InfiniteScrollPaginatorProps, ref: React.ForwardedRef) { const { children, className, + element: Component = 'div' as E, listenToScroll, loadNextDebounceMs = 500, loadNextOnScrollToBottom, @@ -43,7 +72,7 @@ export const InfiniteScrollPaginator = ( ...componentProps } = props; - const rootRef = useRef(null); + const rootRef = useRef(null); const childRef = useRef(null); const scrollListener = useMemo( @@ -114,15 +143,24 @@ export const InfiniteScrollPaginator = ( }; }, [useCapture]); - return ( -
-
- {children} -
-
+ return renderPolymorphic( + Component as E, + { + ...(componentProps as React.ComponentPropsWithRef), + className: clsx('str-chat__infinite-scroll-paginator', className), + ref: (node: React.ComponentRef | null) => { + if (typeof ref === 'function') { + ref(node); + } else if (ref && 'current' in ref) { + (ref as React.RefObject | null>).current = node; + } + rootRef.current = node && node instanceof HTMLElement ? node : null; + }, + }, + React.createElement( + 'div', + { className: 'str-chat__infinite-scroll-paginator__content', ref: childRef }, + children, + ), ); -}; +}) as InfiniteScrollPaginatorComponent; diff --git a/src/components/MediaRecorder/hooks/useMediaRecorder.ts b/src/components/MediaRecorder/hooks/useMediaRecorder.ts index 6438dc2b35..fd85714b78 100644 --- a/src/components/MediaRecorder/hooks/useMediaRecorder.ts +++ b/src/components/MediaRecorder/hooks/useMediaRecorder.ts @@ -6,6 +6,7 @@ import { useMessageComposer } from '../../MessageInput'; import type { LocalVoiceRecordingAttachment } from 'stream-chat'; import type { CustomAudioRecordingConfig, MediaRecordingState } from '../classes'; import type { MessageInputContextValue } from '../../../context'; +import { useSendMessageFn } from '../../MessageInput/hooks/useSendMessageFn'; export type RecordingController = { completeRecording: () => void; @@ -17,7 +18,7 @@ export type RecordingController = { type UseMediaRecorderParams = Pick< MessageInputContextValue, - 'asyncMessagesMultiSendEnabled' | 'handleSubmit' + 'asyncMessagesMultiSendEnabled' > & { enabled: boolean; generateRecordingTitle?: (mimeType: string) => string; @@ -28,15 +29,15 @@ export const useMediaRecorder = ({ asyncMessagesMultiSendEnabled, enabled, generateRecordingTitle, - handleSubmit, recordingConfig, }: UseMediaRecorderParams): RecordingController => { const { t } = useTranslationContext('useMediaRecorder'); const messageComposer = useMessageComposer(); + const sendMessageFn = useSendMessageFn(); const [recording, setRecording] = useState(); const [recordingState, setRecordingState] = useState(); const [permissionState, setPermissionState] = useState(); - const [isScheduledForSubmit, scheduleForSubmit] = useState(false); + // const [isScheduledForSubmit, scheduleForSubmit] = useState(false); const recorder = useMemo( () => @@ -56,17 +57,18 @@ export const useMediaRecorder = ({ if (!recording) return; await messageComposer.attachmentManager.uploadAttachment(recording); if (!asyncMessagesMultiSendEnabled) { - // FIXME: cannot call handleSubmit() directly as the function has stale reference to attachments - scheduleForSubmit(true); + await sendMessageFn(); + // // FIXME: cannot call handleSubmit() directly as the function has stale reference to attachments + // scheduleForSubmit(true); } recorder.cleanUp(); - }, [asyncMessagesMultiSendEnabled, messageComposer, recorder]); + }, [asyncMessagesMultiSendEnabled, messageComposer, recorder, sendMessageFn]); - useEffect(() => { - if (!isScheduledForSubmit) return; - handleSubmit(); - scheduleForSubmit(false); - }, [handleSubmit, isScheduledForSubmit]); + // useEffect(() => { + // if (!isScheduledForSubmit) return; + // handleSubmit(); + // scheduleForSubmit(false); + // }, [handleSubmit, isScheduledForSubmit]); useEffect(() => { if (!recorder) return; diff --git a/src/components/Message/Message.tsx b/src/components/Message/Message.tsx index 39b0e3f039..1fcb184907 100644 --- a/src/components/Message/Message.tsx +++ b/src/components/Message/Message.tsx @@ -12,7 +12,6 @@ import { usePinHandler, useReactionHandler, useReactionsFetcher, - useRetryHandler, useUserHandler, useUserRole, } from './hooks'; @@ -47,7 +46,6 @@ type MessageContextPropsToPick = | 'handleOpenThread' | 'handlePin' | 'handleReaction' - | 'handleRetry' | 'mutes' | 'onMentionsClickMessage' | 'onMentionsHoverMessage' @@ -74,7 +72,7 @@ const MessageWithContext = (props: MessageWithContextProps) => { } = props; const { client, isMessageAIGenerated } = useChatContext('Message'); - const { channelConfig, read } = useChannelStateContext('Message'); + const { channel, channelConfig } = useChannelStateContext('Message'); const { Message: contextMessage } = useComponentContext('Message'); const actionsEnabled = message.type === 'regular' && message.status === 'received'; @@ -104,13 +102,13 @@ const MessageWithContext = (props: MessageWithContextProps) => { !!( !isMyMessage && client.user?.id && - read && - (!read[client.user.id] || + channel.state.read && + (!channel.state.read[client.user.id] || (message?.created_at && new Date(message.created_at).getTime() > - read[client.user.id].last_read.getTime())) + channel.state.read[client.user.id].last_read.getTime())) ), - [client, isMyMessage, message.created_at, read], + [client, isMyMessage, message.created_at, channel], ); const messageActionsHandler = useCallback( @@ -208,18 +206,16 @@ export const Message = (props: MessageProps) => { openThread: propOpenThread, pinPermissions, reactionDetailsSort, - retrySendMessage: propRetrySendMessage, sortReactionDetails, sortReactions, } = props; const { addNotification } = useChannelActionContext('Message'); - const { highlightedMessageId, mutes } = useChannelStateContext('Message'); + const { mutes } = useChannelStateContext('Message'); const handleAction = useActionHandler(message); const handleOpenThread = useOpenThreadHandler(message, propOpenThread); const handleReaction = useReactionHandler(message); - const handleRetry = useRetryHandler(propRetrySendMessage); const userRoles = useUserRole(message, onlySenderCanEdit, disableQuotedMessages); const handleFetchReactions = useReactionsFetcher(message, { @@ -260,7 +256,7 @@ export const Message = (props: MessageProps) => { notify: addNotification, }); - const highlighted = highlightedMessageId === message.id; + // const highlighted = highlightedMessageId === message.id; return ( { handleOpenThread={handleOpenThread} handlePin={handlePin} handleReaction={handleReaction} - handleRetry={handleRetry} - highlighted={highlighted} + // highlighted={highlighted} initialMessage={props.initialMessage} lastOwnMessage={props.lastOwnMessage} lastReceivedId={props.lastReceivedId} diff --git a/src/components/Message/MessageSimple.tsx b/src/components/Message/MessageSimple.tsx index 9bf01d1666..106dcb8b16 100644 --- a/src/components/Message/MessageSimple.tsx +++ b/src/components/Message/MessageSimple.tsx @@ -14,7 +14,7 @@ import { StreamedMessageText as DefaultStreamedMessageText } from './StreamedMes import { isDateSeparatorMessage } from '../MessageList'; import { MessageIsThreadReplyInChannelButtonIndicator as DefaultMessageIsThreadReplyInChannelButtonIndicator } from './MessageIsThreadReplyInChannelButtonIndicator'; import { ReminderNotification as DefaultReminderNotification } from './ReminderNotification'; -import { useMessageReminder } from './hooks'; +import { useMessageReminder, useRetryHandler } from './hooks'; import { areMessageUIPropsEqual, isMessageBlocked, @@ -50,7 +50,6 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => { groupedByUser, handleAction, handleOpenThread, - handleRetry, highlighted, isMessageAIGenerated, isMyMessage, @@ -65,6 +64,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => { const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false); const [isEditedTimestampOpen, setEditedTimestampOpen] = useState(false); const reminder = useMessageReminder(message.id); + const handleRetry = useRetryHandler(); const { Attachment = DefaultAttachment, @@ -125,7 +125,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => { let handleClick: (() => void) | undefined = undefined; if (allowRetry) { - handleClick = () => handleRetry(message); + handleClick = () => handleRetry({ localMessage: message }); } else if (isBounced) { handleClick = () => setIsBounceDialogOpen(true); } else if (isEdited) { diff --git a/src/components/Message/QuotedMessage.tsx b/src/components/Message/QuotedMessage.tsx index db2cf19087..96575de738 100644 --- a/src/components/Message/QuotedMessage.tsx +++ b/src/components/Message/QuotedMessage.tsx @@ -7,12 +7,12 @@ import { Avatar as DefaultAvatar } from '../Avatar'; import { Poll } from '../Poll'; import { useChatContext } from '../../context/ChatContext'; import { useComponentContext } from '../../context/ComponentContext'; +import type { MessageContextValue } from '../../context/MessageContext'; import { useMessageContext } from '../../context/MessageContext'; import { useTranslationContext } from '../../context/TranslationContext'; -import { useChannelActionContext } from '../../context/ChannelActionContext'; import { renderText as defaultRenderText } from './renderText'; -import type { MessageContextValue } from '../../context/MessageContext'; import { useActionHandler } from './'; +import { useMessagePaginator } from '../../hooks'; export type QuotedMessageProps = Pick; @@ -26,9 +26,8 @@ export const QuotedMessage = ({ renderText: propsRenderText }: QuotedMessageProp renderText: contextRenderText, } = useMessageContext('QuotedMessage'); const { t, userLanguage } = useTranslationContext('QuotedMessage'); - const { jumpToMessage } = useChannelActionContext('QuotedMessage'); const actionHandler = useActionHandler(message); - + const messagePaginator = useMessagePaginator(); const renderText = propsRenderText ?? contextRenderText ?? defaultRenderText; const Avatar = ContextAvatar || DefaultAvatar; @@ -65,7 +64,7 @@ export const QuotedMessage = ({ renderText: propsRenderText }: QuotedMessageProp onClickCapture={(e) => { e.stopPropagation(); e.preventDefault(); - jumpToMessage(quoted_message.id); + messagePaginator.jumpToMessage(quoted_message.id); }} > {quoted_message.user && ( diff --git a/src/components/Message/hooks/useRetryHandler.ts b/src/components/Message/hooks/useRetryHandler.ts index 45d7c33d36..71468ccee0 100644 --- a/src/components/Message/hooks/useRetryHandler.ts +++ b/src/components/Message/hooks/useRetryHandler.ts @@ -1,17 +1,20 @@ -import type { RetrySendMessage } from '../../../context/ChannelActionContext'; -import { useChannelActionContext } from '../../../context/ChannelActionContext'; +import { useThreadContext } from '../../Threads'; +import { useChannelStateContext } from '../../../context'; +import type { RetrySendMessageWithLocalUpdateParams } from 'stream-chat'; +import { useCallback } from 'react'; -export const useRetryHandler = ( - customRetrySendMessage?: RetrySendMessage, -): RetrySendMessage => { - const { retrySendMessage: contextRetrySendMessage } = - useChannelActionContext('useRetryHandler'); +export type RetryHandler = ( + params: RetrySendMessageWithLocalUpdateParams, +) => Promise; - const retrySendMessage = customRetrySendMessage || contextRetrySendMessage; +export const useRetryHandler = (): RetryHandler => { + const { channel } = useChannelStateContext(); + const thread = useThreadContext(); - return async (message) => { - if (message) { - await retrySendMessage(message); - } - }; + return useCallback( + async (params: RetrySendMessageWithLocalUpdateParams) => { + await (thread ?? channel).retrySendMessageWithLocalUpdate(params); + }, + [channel, thread], + ); }; diff --git a/src/components/Message/types.ts b/src/components/Message/types.ts index c32669cb60..b14731f438 100644 --- a/src/components/Message/types.ts +++ b/src/components/Message/types.ts @@ -1,11 +1,9 @@ import type { TFunction } from 'i18next'; import type { ReactNode } from 'react'; -import type { ReactionSort, UserResponse } from 'stream-chat'; +import type { LocalMessage, ReactionSort, UserResponse } from 'stream-chat'; import type { PinPermissions, UserEventHandler } from './hooks'; import type { MessageActionsArray } from './utils'; - -import type { LocalMessage } from 'stream-chat'; import type { GroupStyle } from '../MessageList/utils'; import type { MessageInputProps } from '../MessageInput/MessageInput'; import type { ReactionDetailsComparator, ReactionsComparator } from '../Reactions/types'; @@ -99,8 +97,9 @@ export type MessageProps = { mentioned_users?: UserResponse[], options?: RenderTextOptions, ) => ReactNode; - /** Custom retry send message handler to override default in [ChannelActionContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_action_context/) */ - retrySendMessage?: ChannelActionContextValue['retrySendMessage']; + // todo: document how to register custom CustomSendMessageRequestFn with Channel and Thread through StreamChat + // /** Custom retry send message handler to override default in [ChannelActionContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_action_context/) */ + // retrySendMessage?: CustomSendMessageRequestFn; /** Keep track of read receipts for each message sent by the user. When disabled, only the last own message delivery / read status is rendered. */ returnAllReadData?: boolean; /** Comparator function to sort the list of reacted users diff --git a/src/components/MessageInput/EditMessageForm.tsx b/src/components/MessageInput/EditMessageForm.tsx index 215b154d48..9d1416ff7d 100644 --- a/src/components/MessageInput/EditMessageForm.tsx +++ b/src/components/MessageInput/EditMessageForm.tsx @@ -5,10 +5,13 @@ import { Modal as DefaultModal } from '../Modal'; import { useComponentContext, useMessageContext, - useMessageInputContext, useTranslationContext, } from '../../context'; -import { useMessageComposer, useMessageComposerHasSendableData } from './hooks'; +import { + useMessageComposer, + useMessageComposerHasSendableData, + useUpdateMessageFn, +} from './hooks'; import type { MessageUIComponentProps } from '../Message'; @@ -30,34 +33,32 @@ const EditMessageFormSendButton = () => { export const EditMessageForm = () => { const { t } = useTranslationContext('EditMessageForm'); const messageComposer = useMessageComposer(); - const { clearEditingState, handleSubmit } = useMessageInputContext('EditMessageForm'); - - const cancel = useCallback(() => { - clearEditingState?.(); - messageComposer.restore(); - }, [clearEditingState, messageComposer]); + const updateMessage = useUpdateMessageFn(); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') cancel(); + if (event.key === 'Escape') messageComposer.restore(); }; document.addEventListener('keydown', onKeyDown); return () => document.removeEventListener('keydown', onKeyDown); - }, [cancel]); + }, [messageComposer]); return (
{ + e.preventDefault(); + updateMessage(); + }} >