sforkowany z mirror/soapbox
Add infinite scroll to ChatList
rodzic
e384d1f40d
commit
01167af69e
|
@ -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<IChatBox> = ({ 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<IChatBox> = ({ chat, onSetInputRef, autosize, inputRef }
|
|||
// );
|
||||
};
|
||||
|
||||
if (!chatMessageIds) return null;
|
||||
|
||||
return (
|
||||
<Stack className='overflow-hidden flex flex-grow' onMouseOver={handleMouseOver}>
|
||||
<div className='flex-grow h-full overflow-hidden flex justify-center'>
|
||||
<ChatMessageList chatMessageIds={chatMessageIds} chat={chat} autosize />
|
||||
<ChatMessageList chat={chat} autosize />
|
||||
</div >
|
||||
|
||||
<div className='mt-auto p-4 shadow-3xl'>
|
||||
|
|
|
@ -19,18 +19,15 @@ interface IChatList {
|
|||
const ChatList: React.FC<IChatList> = ({ 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<IChatList> = ({ onClickChat, useWindowScroll = false })
|
|||
)}
|
||||
components={{
|
||||
ScrollSeekPlaceholder: () => <PlaceholderChat />,
|
||||
// Footer: () => hasMore ? <PlaceholderChat /> : null,
|
||||
// Footer: () => hasNextPage ? <Spinner withText={false} /> : null,
|
||||
EmptyPlaceholder: renderEmpty,
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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<string, ChatMessageEntity>, chatMessageIds: ImmutableOrderedSet<string>) => (
|
||||
chatMessageIds.reduce((acc, curr) => {
|
||||
const chatMessage = chatMessages.get(curr);
|
||||
return chatMessage ? acc.push(chatMessage) : acc;
|
||||
}, ImmutableList<ChatMessageEntity>())
|
||||
)],
|
||||
chatMessages => chatMessages,
|
||||
);
|
||||
|
||||
interface IChatMessageList {
|
||||
/** Chat the messages are being rendered from. */
|
||||
chat: IChat,
|
||||
/** Message IDs to render. */
|
||||
chatMessageIds: ImmutableOrderedSet<string>,
|
||||
/** Whether to make the chatbox fill the height of the screen. */
|
||||
autosize?: boolean,
|
||||
}
|
||||
|
||||
/** Scrollable list of chat messages. */
|
||||
const ChatMessageList: React.FC<IChatMessageList> = ({ chat, chatMessageIds, autosize }) => {
|
||||
const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const account = useOwnAccount();
|
||||
|
@ -86,9 +67,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ 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<IChatMessageList> = ({ chat, chatMessageIds, aut
|
|||
const lastComputedScroll = useRef<number | undefined>(undefined);
|
||||
const scrollBottom = useRef<number | undefined>(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<IChatMessageList> = ({ 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<IChatMessageList> = ({ chat, chatMessageIds, aut
|
|||
// return emojify(formatted, emojiMap.toJS());
|
||||
};
|
||||
|
||||
const renderDivider = (key: React.Key, text: string) => <Divider text={text} textSize='sm' />;
|
||||
const renderDivider = (key: React.Key, text: string) => <Divider key={key} text={text} textSize='sm' />;
|
||||
|
||||
const handleReportUser = (userId: string) => {
|
||||
return () => {
|
||||
|
@ -370,14 +344,14 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ 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<IChatMessageList> = ({ chat, chatMessageIds, aut
|
|||
<ChatMessageListIntro />
|
||||
) : null}
|
||||
|
||||
{isFetchingNextPage ? (
|
||||
<div className='flex items-center justify-center'>
|
||||
<Spinner size={30} withText={false} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className='flex-grow flex flex-col justify-end space-y-4'>
|
||||
{isLoading ? (
|
||||
<>
|
||||
|
|
|
@ -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 = () => {
|
|||
<button
|
||||
key={account.id}
|
||||
type='button'
|
||||
className='px-4 py-2 w-full flex flex-col hover:bg-gray-100'
|
||||
className='px-4 py-2 w-full flex flex-col hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
onClick={() => {
|
||||
handleClickOnSearchResult.mutate(account.id);
|
||||
clearValue();
|
||||
|
@ -88,7 +91,7 @@ const ChatPane = () => {
|
|||
</Stack>
|
||||
);
|
||||
} else {
|
||||
return <ChatList onClickChat={(chat) => setChat(chat)} useWindowScroll={false} />;
|
||||
return <ChatList onClickChat={handleClickChat} useWindowScroll={false} />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import RelativeTimestamp from 'soapbox/components/relative_timestamp';
|
||||
import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import VerificationBadge from 'soapbox/components/verification_badge';
|
||||
|
||||
|
@ -11,6 +12,11 @@ interface IChatInterface {
|
|||
}
|
||||
|
||||
const Chat: React.FC<IChatInterface> = ({ chat, onClick }) => {
|
||||
// Temporary: remove once bad Staging data is removed.
|
||||
if (!chat.account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={chat.id}
|
||||
|
@ -18,16 +24,33 @@ const Chat: React.FC<IChatInterface> = ({ chat, onClick }) => {
|
|||
onClick={() => onClick(chat)}
|
||||
className='px-4 py-2 w-full flex flex-col hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Avatar src={chat.account?.avatar} size={40} />
|
||||
<HStack alignItems='center' justifyContent='between' space={2} className='w-full'>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Avatar src={chat.account?.avatar} size={40} />
|
||||
|
||||
<Stack alignItems='start'>
|
||||
<div className='flex items-center space-x-1 flex-grow'>
|
||||
<Text weight='bold' size='sm' truncate>{chat.account?.display_name}</Text>
|
||||
{chat.account?.verified && <VerificationBadge />}
|
||||
</div>
|
||||
<Text size='sm' weight='medium' theme='muted' truncate>@{chat.account?.acct}</Text>
|
||||
</Stack>
|
||||
<Stack alignItems='start'>
|
||||
<div className='flex items-center space-x-1 flex-grow'>
|
||||
<Text weight='bold' size='sm' truncate>{chat.account?.display_name || `@${chat.account.username}`}</Text>
|
||||
{chat.account?.verified && <VerificationBadge />}
|
||||
</div>
|
||||
|
||||
{chat.last_message?.content && (
|
||||
<Text size='sm' weight='medium' theme='muted' truncate className='max-w-[200px]'>
|
||||
{chat.last_message?.content}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</HStack>
|
||||
|
||||
{chat.last_message && (
|
||||
<HStack alignItems='center' space={2}>
|
||||
{chat.last_message.unread && (
|
||||
<div className='w-2 h-2 rounded-full bg-secondary-500' />
|
||||
)}
|
||||
|
||||
<RelativeTimestamp timestamp={chat.last_message.created_at} size='sm' />
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
</button>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
})}
|
||||
>
|
||||
<div style={{ width: 50, height: 12 }} className='rounded-full bg-primary-50 dark:bg-primary-800' />
|
||||
<span style={{ width: 50, height: 12 }} className='rounded-full bg-primary-50 dark:bg-primary-800 block' />
|
||||
</Text>
|
||||
|
||||
<div className={classNames({ 'order-1': !isMyMessage })}>
|
||||
|
@ -66,4 +66,4 @@ const PlaceholderChat = ({ isMyMessage = false }: { isMyMessage?: boolean }) =>
|
|||
);
|
||||
};
|
||||
|
||||
export default PlaceholderChat;
|
||||
export default PlaceholderChatMessage;
|
||||
|
|
|
@ -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<IChat[]>(['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<IChat[]>(
|
||||
(prev: IChat[], curr) => [...prev, ...curr.result],
|
||||
[],
|
||||
);
|
||||
|
||||
const chatsQuery = {
|
||||
...queryInfo,
|
||||
data,
|
||||
};
|
||||
|
||||
const getOrCreateChatByAccountId = (accountId: string) => api.post<IChat>(`/api/v1/pleroma/chats/by-account-id/${accountId}`);
|
||||
|
||||
return { chatsQuery, getOrCreateChatByAccountId };
|
||||
|
|
|
@ -29,8 +29,6 @@ const play = (audio: HTMLAudioElement): void => {
|
|||
}
|
||||
}
|
||||
|
||||
console.log('playing');
|
||||
|
||||
audio.play();
|
||||
};
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue