Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-02-09 19:13:02 +01:00
parent ea4f111785
commit 9c202a5193
11 changed files with 93 additions and 96 deletions

View File

@ -13,7 +13,7 @@ import { useChatContext } from '@/contexts/chat-context';
import Emojify from '@/features/emoji/emojify';
import { useFeatures } from '@/hooks/use-features';
import { useRelationshipQuery } from '@/queries/accounts/use-relationship';
import { useChatActions } from '@/queries/chats';
import { useDeleteChat } from '@/queries/chats';
import { useModalsActions } from '@/stores/modals';
import type { Menu } from '@/components/dropdown-menu';
@ -32,14 +32,14 @@ interface IChatListItemInterface {
onClick: (chat: Chat) => void;
}
const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, onClick }) => {
const ChatListItem: React.FC<IChatListItemInterface> = React.memo(({ chat, onClick }) => {
const { openModal } = useModalsActions();
const intl = useIntl();
const features = useFeatures();
const navigate = useNavigate();
const { isUsingMainChatPage } = useChatContext();
const { deleteChat } = useChatActions(chat?.id as string);
const deleteChat = useDeleteChat(chat?.id as string);
const { data: relationship } = useRelationshipQuery(chat?.account.id);
const isBlocked = relationship?.blocked_by && false;
@ -159,6 +159,6 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, onClick }) => {
</div>
</div>
);
};
});
export { ChatListItem as default };

View File

@ -1,5 +1,5 @@
import clsx from 'clsx';
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { Virtuoso } from 'react-virtuoso';
import PullToRefresh from '@/components/pull-to-refresh';
@ -37,6 +37,14 @@ const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false })
const handleRefresh = () => refetch();
const renderChatListItem = useCallback((_index: number, chat: Chat | 'shoutbox') => {
if (chat === 'shoutbox') {
return <div key='shoutbox' className='px-2'><ChatListShoutbox onClick={onClickChat} /></div>;
}
return <div key={chat.id} className='px-2'><ChatListItem chat={chat} onClick={onClickChat} /></div>;
}, [onClickChat]);
const renderEmpty = () => {
if (isFetching) {
return (
@ -65,11 +73,7 @@ const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false })
useWindowScroll={useWindowScroll}
data={allChats}
endReached={handleLoadMore}
itemContent={(_index, chat) => (
<div className='px-2'>
{chat === 'shoutbox' ? <ChatListShoutbox onClick={onClickChat} /> : <ChatListItem chat={chat} onClick={onClickChat} />}
</div>
)}
itemContent={renderChatListItem}
components={{
ScrollSeekPlaceholder: PlaceholderChat,
Footer: () => hasNextPage ? <Spinner withText={false} /> : null,

View File

@ -10,7 +10,7 @@ import Stack from '@/components/ui/stack';
import Text from '@/components/ui/text';
import PlaceholderChatMessage from '@/features/placeholder/components/placeholder-chat-message';
import { useRelationshipQuery } from '@/queries/accounts/use-relationship';
import { useChatActions, useChatMessages } from '@/queries/chats';
import { useChatMessages, useMarkChatAsRead } from '@/queries/chats';
import ChatMessage from './chat-message';
@ -67,13 +67,13 @@ interface IChatMessageList {
}
/** Scrollable list of chat messages. */
const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
const ChatMessageList: React.FC<IChatMessageList> = React.memo(({ chat }) => {
const intl = useIntl();
const node = useRef<VirtuosoHandle>(null);
const [firstItemIndex, setFirstItemIndex] = useState(START_INDEX - 20);
const { markChatAsRead } = useChatActions(chat.id);
const markChatAsRead = useMarkChatAsRead(chat.id);
const {
data: chatMessages,
fetchNextPage,
@ -140,7 +140,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
return acc;
}, []);
};
const cachedChatMessages = buildCachedMessages();
const cachedChatMessages = useMemo(() => buildCachedMessages(), [chatMessages]);
const initialScrollPositionProps = useMemo(() => {
if (process.env.NODE_ENV === 'test') {
@ -160,7 +160,13 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
return false;
}, [firstItemIndex, hasNextPage, isFetching]);
const renderDivider = (key: React.Key, text: string) => <Divider key={key} text={text} textSize='xs' />;
const renderChatMessage = useCallback((index: number, chatMessage: ChatMessageEntity | { type: 'divider'; text: string }) => {
if ('type' in chatMessage && chatMessage.type === 'divider') {
return <Divider key={index} text={chatMessage.text} textSize='xs' />;
}
return <ChatMessage key={chatMessage.id} chat={chat} chatMessage={chatMessage} />;
}, [chat]);
useEffect(() => {
const lastMessage = formattedChatMessages[formattedChatMessages.length - 1];
@ -177,7 +183,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
* 2) it has not already been read
*/
if (!isMessagePending) {
markChatAsRead(lastMessageId);
markChatAsRead.mutate(lastMessageId);
}
}, [formattedChatMessages.length]);
@ -245,13 +251,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
data={cachedChatMessages}
startReached={handleStartReached}
followOutput='auto'
itemContent={(index, chatMessage) => {
if (chatMessage.type === 'divider') {
return renderDivider(index, (chatMessage as any).text);
} else {
return <ChatMessage chat={chat} chatMessage={chatMessage} />;
}
}}
itemContent={(index, chatMessage) => renderChatMessage(index, chatMessage)}
components={{
List,
Scroller,
@ -267,6 +267,6 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
</div>
</div>
);
};
});
export { ChatMessageList as default, List as ChatMessageListList, Scroller as ChatMessageListScroller };

View File

@ -1,4 +1,3 @@
import { useMutation } from '@tanstack/react-query';
import clsx from 'clsx';
import escape from 'lodash/escape';
import React, { useMemo, useState } from 'react';
@ -12,8 +11,7 @@ import Stack from '@/components/ui/stack';
import Text from '@/components/ui/text';
import { MediaGallery } from '@/features/ui/util/async-components';
import { useAppSelector } from '@/hooks/use-app-selector';
import { ChatKeys, useChatActions } from '@/queries/chats';
import { queryClient } from '@/queries/client';
import { useDeleteChatMessage } from '@/queries/chats';
import { useModalsActions } from '@/stores/modals';
import { stripHTML } from '@/utils/html';
import { onlyEmoji } from '@/utils/rich-content';
@ -46,26 +44,17 @@ interface IChatMessage {
chatMessage: ChatMessageEntity;
}
const ChatMessage = (props: IChatMessage) => {
const ChatMessage: React.FC<IChatMessage> = React.memo((props) => {
const { chat, chatMessage } = props;
const { openModal } = useModalsActions();
const intl = useIntl();
const me = useAppSelector((state) => state.me);
const { deleteChatMessage } = useChatActions(chat.id);
const deleteChatMessage = useDeleteChatMessage(chat.id);
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
const handleDeleteMessage = useMutation({
mutationFn: (chatMessageId: string) => deleteChatMessage(chatMessageId),
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ChatKeys.chatMessages(chat.id),
});
},
});
const content = parseContent(chatMessage);
const isMyMessage = chatMessage.account_id === me;
@ -137,14 +126,14 @@ const ChatMessage = (props: IChatMessage) => {
if (isMyMessage) {
menu.push({
text: intl.formatMessage(messages.delete),
action: () => handleDeleteMessage.mutate(chatMessage.id),
action: () => deleteChatMessage.mutate(chatMessage.id),
icon: require('@phosphor-icons/core/regular/trash.svg'),
destructive: true,
});
} else {
menu.push({
text: intl.formatMessage(messages.deleteForMe),
action: () => handleDeleteMessage.mutate(chatMessage.id),
action: () => deleteChatMessage.mutate(chatMessage.id),
icon: require('@phosphor-icons/core/regular/trash.svg'),
destructive: true,
});
@ -273,6 +262,6 @@ const ChatMessage = (props: IChatMessage) => {
</Stack>
</div>
);
};
});
export { ChatMessage as default };

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { ChatWidgetScreens, useChatContext } from '@/contexts/chat-context';
@ -25,13 +25,13 @@ const ChatPane = () => {
const { screen, changeScreen, isOpen, toggleChatPane } = useChatContext();
const { chatsQuery: { data: chats, isLoading } } = useChats();
const handleClickChat = (nextChat: Chat | 'shoutbox') => {
const handleClickChat = useCallback((nextChat: Chat | 'shoutbox') => {
if (nextChat === 'shoutbox') {
changeScreen(ChatWidgetScreens.SHOUTBOX);
} else {
changeScreen(ChatWidgetScreens.CHAT, nextChat.id);
}
};
}, [changeScreen]);
const renderBody = () => {
if (Number(chats?.length) > 0 || showShoutbox || isLoading) {

View File

@ -10,7 +10,7 @@ import Text from '@/components/ui/text';
import { ChatWidgetScreens, useChatContext } from '@/contexts/chat-context';
import { useFeatures } from '@/hooks/use-features';
import { useUnblockAccountMutation, useRelationshipQuery } from '@/queries/accounts/use-relationship';
import { useChatActions } from '@/queries/chats';
import { useDeleteChat } from '@/queries/chats';
import { useModalsActions } from '@/stores/modals';
import ChatPaneHeader from './chat-pane-header';
@ -30,7 +30,7 @@ const ChatSettings = () => {
const { openModal } = useModalsActions();
const { chat, changeScreen, toggleChatPane } = useChatContext();
const { deleteChat } = useChatActions(chat?.id as string);
const deleteChat = useDeleteChat(chat?.id as string);
const { mutate: unblockAccount } = useUnblockAccountMutation(chat?.account.id!);

View File

@ -5,7 +5,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { uploadMedia } from '@/actions/media';
import Stack from '@/components/ui/stack';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useChatActions } from '@/queries/chats';
import { useCreateChatMessage } from '@/queries/chats';
import toast from '@/toast';
import ChatComposer from './chat-composer';
@ -51,7 +51,7 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { createChatMessage } = useChatActions(chat.id);
const createChatMessage = useCreateChatMessage(chat.id);
const [content, setContent] = useState<string>('');
const [attachment, setAttachment] = useState<MediaAttachment | null>(null);
@ -99,8 +99,6 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
const insertLine = () => setContent(content + '\n');
const handleKeyDown: React.KeyboardEventHandler = (event) => {
markRead();
if (event.key === 'Enter' && event.shiftKey) {
event.preventDefault();
insertLine();
@ -120,13 +118,6 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
}
};
const markRead = () => {
// markAsRead.mutate();
// dispatch(markChatRead(chatId));
};
const handleMouseOver = () => markRead();
const handleRemoveFile = () => {
setAttachment(null);
setResetFileKey(fileKeyGen());
@ -159,7 +150,7 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
}, [chat.id, inputRef?.current]);
return (
<Stack className={clsx('flex grow overflow-hidden', className)} onMouseOver={handleMouseOver}>
<Stack className={clsx('flex grow overflow-hidden', className)}>
<div className='flex h-full grow justify-center overflow-hidden'>
<ChatMessageList key={chat.id} chat={chat} />
</div>

View File

@ -13,7 +13,7 @@ import VerificationBadge from '@/components/verification-badge';
import { chatRoute } from '@/features/ui/router';
import { useFeatures } from '@/hooks/use-features';
import { useUnblockAccountMutation, useRelationshipQuery } from '@/queries/accounts/use-relationship';
import { useChat, useChatActions } from '@/queries/chats';
import { useChat, useDeleteChat } from '@/queries/chats';
import { useModalsActions } from '@/stores/modals';
import Chat from '../../chat';
@ -47,7 +47,7 @@ const ChatsPageChat = () => {
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const { deleteChat } = useChatActions(chat?.id as string);
const deleteChat = useDeleteChat(chat?.id as string);
const isBlocked = !!useRelationshipQuery(chat?.account.id).data?.blocked_by;

View File

@ -1,5 +1,5 @@
import { useNavigate } from '@tanstack/react-router';
import React from 'react';
import React, { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { CardTitle } from '@/components/ui/card';
@ -19,13 +19,13 @@ const ChatsPageSidebar = () => {
const intl = useIntl();
const navigate = useNavigate();
const handleClickChat = (chat: Chat | 'shoutbox') => {
const handleClickChat = useCallback((chat: Chat | 'shoutbox') => {
if (chat === 'shoutbox') {
navigate({ to: '/chats/shoutbox' });
} else {
navigate({ to: '/chats/$chatId', params: { chatId: chat.id } });
}
};
}, [navigate]);
const handleChatCreate = () => {
navigate({ to: '/chats/new' });

View File

@ -1,7 +1,6 @@
import { InfiniteData, keepPreviousData, useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query';
import sumBy from 'lodash/sumBy';
import { type Chat, type ChatMessage as BaseChatMessage, type PaginatedResponse, chatMessageSchema } from 'pl-api';
import { useCallback, useMemo } from 'react';
import * as v from 'valibot';
import { importEntities } from '@/actions/importer';
@ -121,36 +120,38 @@ const useChat = (chatId?: string) => {
});
};
const useChatActions = (chatId: string) => {
const { account } = useOwnAccount();
const useMarkChatAsRead = (chatId: string) => {
const client = useClient();
const { setUnreadChatsCount } = useStatContext();
const { chat, changeScreen } = useChatContext();
return useMutation({
mutationFn: (lastReadId: string) => client.chats.markChatAsRead(chatId, lastReadId),
onSuccess: (data) => {
updatePageItem(['chats', 'search'], data, (o, n) => o.id === n.id);
const queryData = queryClient.getQueryData<InfiniteData<PaginatedResponse<unknown>>>(['chats', 'search']);
const markChatAsRead = async (lastReadId: string) =>
client.chats.markChatAsRead(chatId, lastReadId)
.then((data) => {
updatePageItem(['chats', 'search'], data, (o, n) => o.id === n.id);
const queryData = queryClient.getQueryData<InfiniteData<PaginatedResponse<unknown>>>(['chats', 'search']);
if (queryData) {
const flattenedQueryData: any = flattenPages(queryData)?.map((chat: any) => {
if (chat.id === data.id) {
return data;
} else {
return chat;
}
});
setUnreadChatsCount(sumBy(flattenedQueryData, (chat: Chat) => chat.unread));
}
},
});
};
if (queryData) {
const flattenedQueryData: any = flattenPages(queryData)?.map((chat: any) => {
if (chat.id === data.id) {
return data;
} else {
return chat;
}
});
setUnreadChatsCount(sumBy(flattenedQueryData, (chat: Chat) => chat.unread));
}
const useCreateChatMessage = (chatId: string) => {
const { account } = useOwnAccount();
const client = useClient();
return data;
})
.catch(() => null);
const { chat } = useChatContext();
const createChatMessage = useMutation({
return useMutation({
mutationFn: ({ chatId, content, mediaId }: { chatId: string; content: string; mediaId?: string }) =>
client.chats.createChatMessage(chatId, { content, media_id: mediaId }),
retry: false,
@ -212,9 +213,14 @@ const useChatActions = (chatId: string) => {
reOrderChatListItems();
},
});
const deleteChatMessage = useCallback((chatMessageId: string) => client.chats.deleteChatMessage(chatId, chatMessageId), [chatId]);
};
const deleteChat = useMutation({
const useDeleteChat = (chatId: string) => {
const client = useClient();
const { changeScreen } = useChatContext();
return useMutation({
mutationFn: () => client.chats.deleteChat(chatId),
onSuccess() {
changeScreen(ChatWidgetScreens.INBOX);
@ -222,13 +228,19 @@ const useChatActions = (chatId: string) => {
queryClient.invalidateQueries({ queryKey: ['chats', 'search'] });
},
});
return useMemo(() => ({
createChatMessage,
deleteChat,
deleteChatMessage,
markChatAsRead,
}), [createChatMessage, deleteChat, deleteChatMessage, markChatAsRead]);
};
export { ChatKeys, useChat, useChatActions, useChats, useChatMessages };
const useDeleteChatMessage = (chatId: string) => {
const client = useClient();
return useMutation({
mutationFn: (chatMessageId: string) => client.chats.deleteChatMessage(chatId, chatMessageId),
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ChatKeys.chatMessages(chatId),
});
},
});
};
export { ChatKeys, useChat, useMarkChatAsRead, useCreateChatMessage, useDeleteChat, useDeleteChatMessage, useChats, useChatMessages };

View File

@ -208,6 +208,7 @@
@apply py-1.5 pr-4 rtl:pl-4 rtl:pr-0;
> :first-child {
word-break: break-word;
@apply text-gray-900 dark:text-gray-100;
}
}