diff --git a/app/soapbox/features/chats/components/chat-box.tsx b/app/soapbox/features/chats/components/chat-box.tsx index 080e8df01..db3b71fe9 100644 --- a/app/soapbox/features/chats/components/chat-box.tsx +++ b/app/soapbox/features/chats/components/chat-box.tsx @@ -1,6 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import React, { MutableRefObject, useRef, useState } from 'react'; +import React, { MutableRefObject, useEffect, useRef, useState } from 'react'; import { useIntl, defineMessages } from 'react-intl'; import { @@ -56,7 +56,6 @@ const ChatBox: React.FC = ({ chat, onSetInputRef, autosize, inputRef } const isSubmitDisabled = content.length === 0 && !attachment; - // TODO: needs last_read_id param const markAsRead = useMutation(() => markChatAsRead(), { onSuccess: () => { queryClient.invalidateQueries(['chats']); @@ -216,12 +215,10 @@ const ChatBox: React.FC = ({ chat, onSetInputRef, autosize, inputRef } // ); }; - if (!chatMessageIds) return null; - return (
- +
diff --git a/app/soapbox/features/chats/components/chat-list.tsx b/app/soapbox/features/chats/components/chat-list.tsx index 027bc00a8..6965b16a9 100644 --- a/app/soapbox/features/chats/components/chat-list.tsx +++ b/app/soapbox/features/chats/components/chat-list.tsx @@ -19,18 +19,15 @@ interface IChatList { const ChatList: React.FC = ({ onClickChat, useWindowScroll = false }) => { const dispatch = useDispatch(); - const { chatsQuery: { data: chats, isFetching } } = useChats(); + const { chatsQuery: { data: chats, isFetching, hasNextPage, fetchNextPage } } = useChats(); const isEmpty = chats?.length === 0; - // const handleLoadMore = useCallback(() => { - // if (hasMore && !isLoading) { - // dispatch(expandChats()); - // } - // }, [dispatch, hasMore, isLoading]); - - const handleLoadMore = () => console.log('load more'); - + const handleLoadMore = () => { + if (hasNextPage && !isFetching) { + fetchNextPage(); + } + }; const handleRefresh = () => { return dispatch(fetchChats()) as any; @@ -62,7 +59,7 @@ const ChatList: React.FC = ({ onClickChat, useWindowScroll = false }) )} components={{ ScrollSeekPlaceholder: () => , - // Footer: () => hasMore ? : null, + // Footer: () => hasNextPage ? : null, EmptyPlaceholder: renderEmpty, }} /> diff --git a/app/soapbox/features/chats/components/chat-message-list.tsx b/app/soapbox/features/chats/components/chat-message-list.tsx index e397fe532..7d935be7c 100644 --- a/app/soapbox/features/chats/components/chat-message-list.tsx +++ b/app/soapbox/features/chats/components/chat-message-list.tsx @@ -1,27 +1,20 @@ import { useMutation } from '@tanstack/react-query'; import classNames from 'clsx'; -import { - Map as ImmutableMap, - List as ImmutableList, - OrderedSet as ImmutableOrderedSet, -} from 'immutable'; +import { List as ImmutableList } from 'immutable'; import escape from 'lodash/escape'; import throttle from 'lodash/throttle'; -import React, { useState, useEffect, useRef, useLayoutEffect, useMemo } from 'react'; +import React, { useState, useEffect, useRef, useLayoutEffect } from 'react'; import { useIntl, defineMessages } from 'react-intl'; -import { createSelector } from 'reselect'; -import { fetchChatMessages, deleteChatMessage } from 'soapbox/actions/chats'; import { openModal } from 'soapbox/actions/modals'; -import { initReport, initReportById } from 'soapbox/actions/reports'; -import { Avatar, Button, Divider, HStack, IconButton, Spinner, Stack, Text } from 'soapbox/components/ui'; +import { initReportById } from 'soapbox/actions/reports'; +import { Avatar, Divider, HStack, Spinner, Stack, Text } from 'soapbox/components/ui'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; -import { useChatContext } from 'soapbox/contexts/chat-context'; -import emojify from 'soapbox/features/emoji/emoji'; +// import emojify from 'soapbox/features/emoji/emoji'; import PlaceholderChatMessage from 'soapbox/features/placeholder/components/placeholder-chat-message'; import Bundle from 'soapbox/features/ui/components/bundle'; import { MediaGallery } from 'soapbox/features/ui/util/async-components'; -import { useAppSelector, useAppDispatch, useRefEventHandler, useOwnAccount } from 'soapbox/hooks'; +import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks'; import { IChat, IChatMessage, useChat, useChatMessages } from 'soapbox/queries/chats'; import { queryClient } from 'soapbox/queries/client'; import { onlyEmoji } from 'soapbox/utils/rich_content'; @@ -58,27 +51,15 @@ const timeChange = (prev: IChatMessage, curr: IChatMessage): TimeFormat | null = // return map.set(`:${emoji.get('shortcode')}:`, emoji); // }, ImmutableMap()); -const getChatMessages = createSelector( - [(chatMessages: ImmutableMap, chatMessageIds: ImmutableOrderedSet) => ( - chatMessageIds.reduce((acc, curr) => { - const chatMessage = chatMessages.get(curr); - return chatMessage ? acc.push(chatMessage) : acc; - }, ImmutableList()) - )], - chatMessages => chatMessages, -); - interface IChatMessageList { /** Chat the messages are being rendered from. */ chat: IChat, - /** Message IDs to render. */ - chatMessageIds: ImmutableOrderedSet, /** Whether to make the chatbox fill the height of the screen. */ autosize?: boolean, } /** Scrollable list of chat messages. */ -const ChatMessageList: React.FC = ({ chat, chatMessageIds, autosize }) => { +const ChatMessageList: React.FC = ({ chat, autosize }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const account = useOwnAccount(); @@ -86,9 +67,7 @@ const ChatMessageList: React.FC = ({ chat, chatMessageIds, aut const [initialLoad, setInitialLoad] = useState(true); const [scrollPosition, setScrollPosition] = useState(0); - const { needsAcceptance } = useChatContext(); - - const { deleteChatMessage, acceptChat, deleteChat } = useChat(chat.id); + const { deleteChatMessage, markChatAsRead } = useChat(chat.id); const { data: chatMessages, isLoading, isFetching, isFetched, fetchNextPage, isFetchingNextPage, isPlaceholderData } = useChatMessages(chat.id); const formattedChatMessages = chatMessages || []; @@ -99,9 +78,6 @@ const ChatMessageList: React.FC = ({ chat, chatMessageIds, aut const lastComputedScroll = useRef(undefined); const scrollBottom = useRef(undefined); - const initialCount = useMemo(() => formattedChatMessages.length, []); - - const handleDeleteMessage = useMutation((chatMessageId: string) => deleteChatMessage(chatMessageId), { onSettled: () => { queryClient.invalidateQueries(['chats', 'messages', chat.id]); @@ -152,8 +128,6 @@ const ChatMessageList: React.FC = ({ chat, chatMessageIds, aut const restoreScrollPosition = () => { if (node.current && scrollBottom.current) { - console.log('bottom', scrollBottom.current); - lastComputedScroll.current = node.current.scrollHeight - scrollBottom.current; node.current.scrollTop = lastComputedScroll.current; } @@ -227,7 +201,7 @@ const ChatMessageList: React.FC = ({ chat, chatMessageIds, aut // return emojify(formatted, emojiMap.toJS()); }; - const renderDivider = (key: React.Key, text: string) => ; + const renderDivider = (key: React.Key, text: string) => ; const handleReportUser = (userId: string) => { return () => { @@ -370,14 +344,14 @@ const ChatMessageList: React.FC = ({ chat, chatMessageIds, aut // } }, [formattedChatMessages.length]); + useEffect(() => { + markChatAsRead(); + }, [formattedChatMessages.length]); + // useEffect(() => { // scrollToBottom(); // }, [messagesEnd.current]); - // History added. - const lastChatId = Number(chatMessages && chatMessages[0]?.id); - - useEffect(() => { // Restore scroll bar position when loading old messages. if (!initialLoad) { @@ -400,6 +374,12 @@ const ChatMessageList: React.FC = ({ chat, chatMessageIds, aut ) : null} + {isFetchingNextPage ? ( +
+ +
+ ) : null} +
{isLoading ? ( <> diff --git a/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx b/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx index dbe180c3f..2ab917f25 100644 --- a/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx +++ b/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx @@ -9,7 +9,8 @@ import { Avatar, HStack, Icon, Input, Stack, Text } from 'soapbox/components/ui' import VerificationBadge from 'soapbox/components/verification_badge'; import { useChatContext } from 'soapbox/contexts/chat-context'; import { useAppDispatch, useDebounce } from 'soapbox/hooks'; -import { useChats } from 'soapbox/queries/chats'; +import { IChat, useChats } from 'soapbox/queries/chats'; +import { queryClient } from 'soapbox/queries/client'; import useAccountSearch from 'soapbox/queries/search'; import ChatList from '../chat-list'; @@ -48,9 +49,11 @@ const ChatPane = () => { }, onSuccess: (response) => { setChat(response.data); + queryClient.invalidateQueries(['chats']); }, }); + const handleClickChat = (chat: IChat) => setChat(chat); const clearValue = () => { if (hasSearchValue) { @@ -66,7 +69,7 @@ const ChatPane = () => { ); diff --git a/app/soapbox/features/placeholder/components/placeholder-chat-message.tsx b/app/soapbox/features/placeholder/components/placeholder-chat-message.tsx index 86670d1ed..abde1320f 100644 --- a/app/soapbox/features/placeholder/components/placeholder-chat-message.tsx +++ b/app/soapbox/features/placeholder/components/placeholder-chat-message.tsx @@ -8,7 +8,7 @@ import { randomIntFromInterval } from '../utils'; import PlaceholderAvatar from './placeholder_avatar'; /** Fake chat to display while data is loading. */ -const PlaceholderChat = ({ isMyMessage = false }: { isMyMessage?: boolean }) => { +const PlaceholderChatMessage = ({ isMyMessage = false }: { isMyMessage?: boolean }) => { const messageLength = randomIntFromInterval(160, 220); return ( @@ -55,7 +55,7 @@ const PlaceholderChat = ({ isMyMessage = false }: { isMyMessage?: boolean }) => 'order-2': !isMyMessage, })} > -
+
@@ -66,4 +66,4 @@ const PlaceholderChat = ({ isMyMessage = false }: { isMyMessage?: boolean }) => ); }; -export default PlaceholderChat; +export default PlaceholderChatMessage; diff --git a/app/soapbox/queries/chats.ts b/app/soapbox/queries/chats.ts index a695677cd..c723aef21 100644 --- a/app/soapbox/queries/chats.ts +++ b/app/soapbox/queries/chats.ts @@ -1,4 +1,4 @@ -import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery, useMutation } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import snackbar from 'soapbox/actions/snackbar'; @@ -11,7 +11,15 @@ export interface IChat { id: string unread: number created_by_account: string - last_message: null | string + last_message: null | { + account_id: string + chat_id: string + content: string + created_at: string + discarded_at: string | null + id: string + unread: boolean + } created_at: Date updated_at: Date accepted: boolean @@ -76,18 +84,48 @@ const useChatMessages = (chatId: string) => { data, }; }; + const useChats = () => { const api = useApi(); - const getChats = async() => { - const { data } = await api.get('/api/v1/pleroma/chats'); - return data; + const getChats = async(pageParam?: any): Promise<{ result: IChat[], maxId: string, hasMore: boolean }> => { + const { data, headers } = await api.get('/api/v1/pleroma/chats', { + params: { + max_id: pageParam?.maxId, + }, + }); + + const hasMore = !!headers.link; + const nextMaxId = data[data.length - 1]?.id; + + return { + result: data, + maxId: nextMaxId, + hasMore, + }; }; - const chatsQuery = useQuery(['chats'], getChats, { - placeholderData: [], + const queryInfo = useInfiniteQuery(['chats'], ({ pageParam }) => getChats(pageParam), { + keepPreviousData: true, + getNextPageParam: (config) => { + if (config.hasMore) { + return { maxId: config.maxId }; + } + + return undefined; + }, }); + const data = queryInfo.data?.pages.reduce( + (prev: IChat[], curr) => [...prev, ...curr.result], + [], + ); + + const chatsQuery = { + ...queryInfo, + data, + }; + const getOrCreateChatByAccountId = (accountId: string) => api.post(`/api/v1/pleroma/chats/by-account-id/${accountId}`); return { chatsQuery, getOrCreateChatByAccountId }; diff --git a/app/soapbox/utils/sounds.ts b/app/soapbox/utils/sounds.ts index 40e396080..af4fc567f 100644 --- a/app/soapbox/utils/sounds.ts +++ b/app/soapbox/utils/sounds.ts @@ -29,8 +29,6 @@ const play = (audio: HTMLAudioElement): void => { } } - console.log('playing'); - audio.play(); };