Move query into Context

environments/review-chats-g56n7m/deployments/1250
Chewbacca 2022-11-02 15:28:16 -04:00
rodzic 9869cf6f55
commit 54466f1293
15 zmienionych plików z 117 dodań i 86 usunięć

Wyświetl plik

@ -2,7 +2,7 @@ import { InfiniteData } from '@tanstack/react-query';
import { getSettings } from 'soapbox/actions/settings';
import messages from 'soapbox/locales/messages';
import { ChatKeys, isLastMessage } from 'soapbox/queries/chats';
import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats';
import { queryClient } from 'soapbox/queries/client';
import { updatePageItem, appendPageItem, removePageItem, flattenPages, PaginatedResult } from 'soapbox/utils/queries';
import { play, soundCache } from 'soapbox/utils/sounds';
@ -91,6 +91,21 @@ const removeChatMessage = (payload: string) => {
removePageItem(ChatKeys.chatMessages(chatId), chatMessageId, (o: any, n: any) => String(o.id) === String(n));
};
// Update the specific Chat query data.
const updateChatQuery = (chat: IChat) => {
const cachedChat = queryClient.getQueryData<IChat>(ChatKeys.chat(chat.id));
if (!cachedChat) {
return;
}
const newChat = {
...cachedChat,
latest_read_message_by_account: chat.latest_read_message_by_account,
latest_read_message_created_at: chat.latest_read_message_created_at,
};
queryClient.setQueryData<Chat>(ChatKeys.chat(chat.id), newChat as any);
};
const connectTimelineStream = (
timelineId: string,
path: string,
@ -152,6 +167,9 @@ const connectTimelineStream = (
case 'chat_message.deleted': // TruthSocial
removeChatMessage(data.payload);
break;
case 'chat_message.read': // TruthSocial
updateChatQuery(JSON.parse(data.payload));
break;
case 'pleroma:follow_relationships_update':
dispatch(updateFollowRelationships(JSON.parse(data.payload)));
break;

Wyświetl plik

@ -3,44 +3,52 @@ import { useDispatch } from 'react-redux';
import { toggleMainWindow } from 'soapbox/actions/chats';
import { useOwnAccount, useSettings } from 'soapbox/hooks';
import type { IChat } from 'soapbox/queries/chats';
import { IChat, useChat } from 'soapbox/queries/chats';
type WindowState = 'open' | 'minimized';
const ChatContext = createContext<any>({
chat: null,
isOpen: false,
isEditing: false,
needsAcceptance: false,
});
enum ChatWidgetScreens {
INBOX = 'INBOX',
SEARCH = 'SEARCH',
CHAT = 'CHAT',
CHAT_SETTINGS = 'CHAT_SETTINGS'
}
const ChatProvider: React.FC = ({ children }) => {
const dispatch = useDispatch();
const settings = useSettings();
const account = useOwnAccount();
const [chat, setChat] = useState<IChat | null>(null);
const [isEditing, setEditing] = useState<boolean>(false);
const [isSearching, setSearching] = useState<boolean>(false);
const [screen, setScreen] = useState<ChatWidgetScreens>(ChatWidgetScreens.INBOX);
const [currentChatId, setCurrentChatId] = useState<null | string>(null);
const { data: chat } = useChat(currentChatId as string);
const mainWindowState = settings.getIn(['chats', 'mainWindow']) as WindowState;
const needsAcceptance = !chat?.accepted && chat?.created_by_account !== account?.id;
const isOpen = mainWindowState === 'open';
const changeScreen = (screen: ChatWidgetScreens, currentChatId?: string | null) => {
setCurrentChatId(currentChatId || null);
setScreen(screen);
};
const toggleChatPane = () => dispatch(toggleMainWindow());
const value = useMemo(() => ({
chat,
setChat,
needsAcceptance,
isOpen,
isEditing,
isSearching,
setEditing,
setSearching,
toggleChatPane,
}), [chat, needsAcceptance, isOpen, isEditing, isSearching]);
screen,
changeScreen,
currentChatId,
}), [chat, currentChatId, needsAcceptance, isOpen, screen, changeScreen]);
return (
<ChatContext.Provider value={value}>
@ -51,16 +59,14 @@ const ChatProvider: React.FC = ({ children }) => {
interface IChatContext {
chat: IChat | null
isEditing: boolean
isOpen: boolean
isSearching: boolean
needsAcceptance: boolean
setChat: React.Dispatch<React.SetStateAction<IChat | null>>
setEditing: React.Dispatch<React.SetStateAction<boolean>>
setSearching: React.Dispatch<React.SetStateAction<boolean>>
toggleChatPane(): void
screen: ChatWidgetScreens
currentChatId: string | null
changeScreen(screen: ChatWidgetScreens, currentChatId?: string | null): void
}
const useChatContext = (): IChatContext => useContext(ChatContext);
export { ChatContext, ChatProvider, useChatContext };
export { ChatContext, ChatProvider, useChatContext, ChatWidgetScreens };

Wyświetl plik

@ -38,6 +38,7 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
const intl = useIntl();
const { chat } = useChatContext();
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocked_by']));
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking']));

Wyświetl plik

@ -1,11 +1,10 @@
import classNames from 'clsx';
import React, { useRef, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Virtuoso } from 'react-virtuoso';
import { fetchChats } from 'soapbox/actions/chats';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { Spinner, Stack, Text } from 'soapbox/components/ui';
import { Spinner, Stack } from 'soapbox/components/ui';
import PlaceholderChat from 'soapbox/features/placeholder/components/placeholder-chat';
import { useAppDispatch } from 'soapbox/hooks';
import { useChats } from 'soapbox/queries/chats';

Wyświetl plik

@ -73,6 +73,8 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const account = useOwnAccount();
const lastReadMessageDateString = chat.latest_read_message_by_account.find((latest) => latest.id === chat.account.id)?.date;
const lastReadMessageTimestamp = lastReadMessageDateString ? new Date(lastReadMessageDateString) : null;
const node = useRef<VirtuosoHandle>(null);
const [firstItemIndex, setFirstItemIndex] = useState(START_INDEX - 20);
@ -211,15 +213,19 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
const renderDivider = (key: React.Key, text: string) => <Divider key={key} text={text} textSize='sm' />;
const handleCopyText = (chatMessage: IChatMessage) => {
const handleCopyText = (chatMessage: ChatMessageEntity) => {
if (navigator.clipboard) {
const text = stripHTML(chatMessage.content);
navigator.clipboard.writeText(text);
}
};
const renderMessage = (chatMessage: any) => {
const renderMessage = (chatMessage: ChatMessageEntity) => {
const isMyMessage = chatMessage.account_id === me;
// did this occur before this time?
const isRead = isMyMessage
&& lastReadMessageTimestamp
&& lastReadMessageTimestamp >= new Date(chatMessage.created_at);
const menu: Menu = [];
@ -241,7 +247,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
} else {
menu.push({
text: intl.formatMessage(messages.report),
action: () => dispatch(initReport(normalizeAccount(chat.account) as any, { chatMessage })),
action: () => dispatch(initReport(normalizeAccount(chat.account) as any, { chatMessage } as any)),
icon: require('@tabler/icons/flag.svg'),
});
menu.push({
@ -336,7 +342,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
{intl.formatTime(chatMessage.created_at)}
</Text>
{isMyMessage && !chatMessage.unread ? (
{isRead ? (
<span className='rounded-full flex flex-col items-center justify-center h-3.5 w-3.5 dark:bg-primary-400 dark:text-primary-900'>
<Icon src={require('@tabler/icons/check.svg')} strokeWidth={3} className='w-2.5 h-2.5' />
</span>

Wyświetl plik

@ -3,7 +3,6 @@ import React, { useEffect, useRef, useState } from 'react';
import { Route, Switch } from 'react-router-dom';
import { Stack } from 'soapbox/components/ui';
import { useChatContext } from 'soapbox/contexts/chat-context';
import { useOwnAccount } from 'soapbox/hooks';
import { useChat } from 'soapbox/queries/chats';
@ -21,8 +20,7 @@ const ChatPage: React.FC<IChatPage> = ({ chatId }) => {
const account = useOwnAccount();
const isOnboarded = account?.chats_onboarded;
const { chat, setChat } = useChatContext();
const { chat: chatQueryResult } = useChat(chatId);
const { data: chat } = useChat(chatId);
const containerRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState<string | number>('100%');
@ -41,14 +39,6 @@ const ChatPage: React.FC<IChatPage> = ({ chatId }) => {
setHeight(fullHeight - top + offset);
};
useEffect(() => {
const data = chatQueryResult?.data;
if (data) {
setChat(data);
}
}, [chatQueryResult?.isLoading]);
useEffect(() => {
calculateHeight();
}, [containerRef.current]);

Wyświetl plik

@ -1,14 +1,14 @@
import React, { useRef } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useHistory, useParams } from 'react-router-dom';
import { blockAccount, unblockAccount } from 'soapbox/actions/accounts';
import { openModal } from 'soapbox/actions/modals';
import List, { ListItem } from 'soapbox/components/list';
import { Avatar, HStack, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Stack, Text } from 'soapbox/components/ui';
import VerificationBadge from 'soapbox/components/verification_badge';
import { useChatContext } from 'soapbox/contexts/chat-context';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { MessageExpirationValues, useChatActions } from 'soapbox/queries/chats';
import { MessageExpirationValues, useChat, useChatActions } from 'soapbox/queries/chats';
import { secondsToDays } from 'soapbox/utils/numbers';
import Chat from '../../chat';
@ -41,10 +41,14 @@ const messages = defineMessages({
const ChatPageMain = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const history = useHistory();
const { chatId } = useParams<{ chatId: string }>();
const { data: chat } = useChat(chatId);
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const { chat, setChat } = useChatContext();
const { deleteChat, updateChat } = useChatActions(chat?.id as string);
const handleUpdateChat = (value: MessageExpirationValues) => updateChat.mutate({ message_expiration: value });
@ -93,7 +97,7 @@ const ChatPageMain = () => {
<IconButton
src={require('@tabler/icons/arrow-left.svg')}
className='sm:hidden h-7 w-7 mr-2 sm:mr-0'
onClick={() => setChat(null)}
onClick={() => history.push('/chats')}
/>
<Avatar src={chat.account?.avatar} size={40} className='flex-none' />

Wyświetl plik

@ -4,7 +4,6 @@ import { useHistory } from 'react-router-dom';
import AccountSearch from 'soapbox/components/account_search';
import { CardTitle, HStack, Stack, Text } from 'soapbox/components/ui';
import { useChatContext } from 'soapbox/contexts/chat-context';
import { ChatKeys, useChats } from 'soapbox/queries/chats';
import { queryClient } from 'soapbox/queries/client';
@ -14,12 +13,10 @@ interface IChatPageNew {
/** New message form to create a chat. */
const ChatPageNew: React.FC<IChatPageNew> = () => {
const history = useHistory();
const { setChat } = useChatContext();
const { getOrCreateChatByAccountId } = useChats();
const handleAccountSelected = async (accountId: string) => {
const { data } = await getOrCreateChatByAccountId(accountId);
setChat(data);
history.push(`/chats/${data.id}`);
queryClient.invalidateQueries(ChatKeys.chatSearch());
};

Wyświetl plik

@ -3,7 +3,6 @@ import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { CardTitle, HStack, IconButton, Stack } from 'soapbox/components/ui';
import { useChatContext } from 'soapbox/contexts/chat-context';
import { useDebounce, useFeatures } from 'soapbox/hooks';
import { IChat } from 'soapbox/queries/chats';
@ -20,12 +19,10 @@ const ChatPageSidebar = () => {
const features = useFeatures();
const [search, setSearch] = useState('');
const { setChat } = useChatContext();
const debouncedSearch = useDebounce(search, 300);
const handleClickChat = (chat: IChat) => {
setChat(chat);
history.push(`/chats/${chat.id}`);
};

Wyświetl plik

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Stack } from 'soapbox/components/ui';
import { useChatContext } from 'soapbox/contexts/chat-context';
import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context';
import { useStatContext } from 'soapbox/contexts/stat-context';
import { useDebounce, useFeatures } from 'soapbox/hooks';
import { IChat, useChats } from 'soapbox/queries/chats';
@ -24,13 +24,13 @@ const ChatPane = () => {
const [value, setValue] = useState<string>();
const debouncedValue = debounce(value as string, 300);
const { chat, setChat, isOpen, isSearching, setSearching, toggleChatPane } = useChatContext();
const { screen, changeScreen, isOpen, toggleChatPane } = useChatContext();
const { chatsQuery: { data: chats, isLoading } } = useChats(debouncedValue);
const hasSearchValue = Number(debouncedValue?.length) > 0;
const handleClickChat = (nextChat: IChat) => {
setChat(nextChat);
changeScreen(ChatWidgetScreens.CHAT, nextChat.id);
setValue(undefined);
};
@ -66,13 +66,17 @@ const ChatPane = () => {
);
} else if (chats?.length === 0) {
return (
<Blankslate onSearch={() => setSearching(true)} />
<Blankslate
onSearch={() => {
changeScreen(ChatWidgetScreens.SEARCH);
}}
/>
);
}
};
// Active chat
if (chat?.id) {
if (screen === ChatWidgetScreens.CHAT || screen === ChatWidgetScreens.CHAT_SETTINGS) {
return (
<Pane isOpen={isOpen} index={0} main>
<ChatWindow />
@ -80,7 +84,7 @@ const ChatPane = () => {
);
}
if (isSearching) {
if (screen === ChatWidgetScreens.SEARCH) {
return <ChatSearch />;
}
@ -92,7 +96,7 @@ const ChatPane = () => {
isOpen={isOpen}
onToggle={toggleChatPane}
secondaryAction={() => {
setSearching(true);
changeScreen(ChatWidgetScreens.SEARCH);
setValue(undefined);
if (!isOpen) {

Wyświetl plik

@ -5,7 +5,7 @@ import { defineMessages, useIntl } from 'react-intl';
import snackbar from 'soapbox/actions/snackbar';
import { HStack, Icon, Input, Stack, Text } from 'soapbox/components/ui';
import { useChatContext } from 'soapbox/contexts/chat-context';
import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context';
import { useAppDispatch, useDebounce } from 'soapbox/hooks';
import { useChats } from 'soapbox/queries/chats';
import { queryClient } from 'soapbox/queries/client';
@ -28,7 +28,7 @@ const ChatSearch = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { isOpen, setChat, setSearching, toggleChatPane } = useChatContext();
const { isOpen, changeScreen, toggleChatPane } = useChatContext();
const { getOrCreateChatByAccountId } = useChats();
const [value, setValue] = useState<string>();
@ -47,7 +47,7 @@ const ChatSearch = () => {
dispatch(snackbar.error(data?.error));
},
onSuccess: (response) => {
setChat(response.data);
changeScreen(ChatWidgetScreens.CHAT, response.data.id);
queryClient.invalidateQueries(ChatKeys.chatSearch());
},
});
@ -82,7 +82,11 @@ const ChatSearch = () => {
data-testid='pane-header'
title={
<HStack alignItems='center' space={2}>
<button onClick={() => setSearching(false)}>
<button
onClick={() => {
changeScreen(ChatWidgetScreens.INBOX);
}}
>
<Icon
src={require('@tabler/icons/arrow-left.svg')}
className='h-6 w-6 text-gray-600 dark:text-gray-400'

Wyświetl plik

@ -5,7 +5,7 @@ import { blockAccount, unblockAccount } from 'soapbox/actions/accounts';
import { openModal } from 'soapbox/actions/modals';
import List, { ListItem } from 'soapbox/components/list';
import { Avatar, HStack, Icon, Select, Stack, Text } from 'soapbox/components/ui';
import { useChatContext } from 'soapbox/contexts/chat-context';
import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { messageExpirationOptions, MessageExpirationValues, useChatActions } from 'soapbox/queries/chats';
import { secondsToDays } from 'soapbox/utils/numbers';
@ -34,14 +34,16 @@ const ChatSettings = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { chat, setEditing, toggleChatPane } = useChatContext();
const { chat, changeScreen, toggleChatPane } = useChatContext();
const { deleteChat, updateChat } = useChatActions(chat?.id as string);
const handleUpdateChat = (value: MessageExpirationValues) => updateChat.mutate({ message_expiration: value });
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking']));
const closeSettings = () => setEditing(false);
const closeSettings = () => {
changeScreen(ChatWidgetScreens.CHAT, chat?.id);
};
const minimizeChatPane = () => {
closeSettings();

Wyświetl plik

@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
import { Avatar, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
import VerificationBadge from 'soapbox/components/verification_badge';
import { useChatContext } from 'soapbox/contexts/chat-context';
import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context';
import { secondsToDays } from 'soapbox/utils/numbers';
import Chat from '../chat';
@ -32,19 +32,22 @@ const LinkWrapper = ({ enabled, to, children }: { enabled: boolean, to: string,
const ChatWindow = () => {
const intl = useIntl();
const { chat, setChat, isOpen, isEditing, needsAcceptance, setEditing, setSearching, toggleChatPane } = useChatContext();
const { chat, currentChatId, screen, changeScreen, isOpen, needsAcceptance, toggleChatPane } = useChatContext();
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const closeChat = () => setChat(null);
const closeChat = () => {
changeScreen(ChatWidgetScreens.INBOX);
};
const openSearch = () => {
toggleChatPane();
setSearching(true);
setChat(null);
changeScreen(ChatWidgetScreens.SEARCH);
};
const openChatSettings = () => setEditing(true);
const openChatSettings = () => {
changeScreen(ChatWidgetScreens.CHAT_SETTINGS, currentChatId);
};
const secondaryAction = () => {
if (needsAcceptance) {
@ -56,7 +59,7 @@ const ChatWindow = () => {
if (!chat) return null;
if (isEditing) {
if (screen === ChatWidgetScreens.CHAT_SETTINGS) {
return <ChatSettings />;
}

Wyświetl plik

@ -28,7 +28,7 @@ const chat: IChat = {
discarded_at: null,
id: '1',
last_message: null,
latest_read_message_by_account: null,
latest_read_message_by_account: [],
latest_read_message_created_at: null,
message_expiration: 1209600,
unread: 0,
@ -169,7 +169,7 @@ describe('useChat()', () => {
});
it('is successful', async () => {
const { result } = renderHook(() => useChat(chat.id).chat);
const { result } = renderHook(() => useChat(chat.id));
await waitFor(() => expect(result.current.isFetching).toBe(false));
@ -185,7 +185,7 @@ describe('useChat()', () => {
});
it('is has error state', async() => {
const { result } = renderHook(() => useChat(chat.id).chat);
const { result } = renderHook(() => useChat(chat.id));
await waitFor(() => expect(result.current.isFetching).toBe(false));

Wyświetl plik

@ -6,7 +6,7 @@ import { importFetchedAccount, importFetchedAccounts } from 'soapbox/actions/imp
import snackbar from 'soapbox/actions/snackbar';
import { getNextLink } from 'soapbox/api';
import compareId from 'soapbox/compare_id';
import { useChatContext } from 'soapbox/contexts/chat-context';
import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context';
import { useStatContext } from 'soapbox/contexts/stat-context';
import { useApi, useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries';
@ -40,8 +40,9 @@ export interface IChat {
id: string
unread: boolean
}
latest_read_message_by_account: null | {
[id: number]: string
latest_read_message_by_account: {
id: string,
date: string
}[]
latest_read_message_created_at: null | string
message_expiration: MessageExpirationValues
@ -177,7 +178,6 @@ const useChats = (search?: string) => {
const useChat = (chatId?: string) => {
const api = useApi();
const actions = useChatActions(chatId!);
const dispatch = useAppDispatch();
const getChat = async () => {
@ -190,9 +190,9 @@ const useChat = (chatId?: string) => {
}
};
const chat = useQuery<IChat | undefined>(ChatKeys.chat(chatId), getChat);
return { ...actions, chat };
return useQuery<IChat | undefined>(ChatKeys.chat(chatId), getChat, {
enabled: !!chatId,
});
};
const useChatActions = (chatId: string) => {
@ -200,7 +200,7 @@ const useChatActions = (chatId: string) => {
const dispatch = useAppDispatch();
const { setUnreadChatsCount } = useStatContext();
const { chat, setChat, setEditing } = useChatContext();
const { chat, changeScreen } = useChatContext();
const markChatAsRead = async (lastReadId: string) => {
return api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId })
@ -239,21 +239,22 @@ const useChatActions = (chatId: string) => {
// Optimistically update to the new value
queryClient.setQueryData(ChatKeys.chat(chatId), nextChat);
setChat(nextChat as IChat);
changeScreen(ChatWidgetScreens.CHAT, nextChat.id);
// Return a context object with the snapshotted value
return { prevChat };
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (_error: any, _newData: any, context: any) => {
setChat(context?.prevChat);
changeScreen(ChatWidgetScreens.CHAT, context.prevChat.id);
queryClient.setQueryData(ChatKeys.chat(chatId), context.prevChat);
dispatch(snackbar.error('Chat Settings failed to update.'));
},
onSuccess(response) {
queryClient.invalidateQueries(ChatKeys.chat(chatId));
queryClient.invalidateQueries(ChatKeys.chatSearch());
setChat(response.data);
changeScreen(ChatWidgetScreens.CHAT, response.data.id);
dispatch(snackbar.success('Chat Settings updated successfully'));
},
});
@ -262,16 +263,15 @@ const useChatActions = (chatId: string) => {
const acceptChat = useMutation(() => api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/accept`), {
onSuccess(response) {
setChat(response.data);
changeScreen(ChatWidgetScreens.CHAT, response.data.id);
queryClient.invalidateQueries(ChatKeys.chatMessages(chatId));
queryClient.invalidateQueries(ChatKeys.chatSearch());
},
});
const deleteChat = useMutation(() => api.delete<IChat>(`/api/v1/pleroma/chats/${chatId}`), {
onSuccess(response) {
setChat(null);
setEditing(false);
onSuccess() {
changeScreen(ChatWidgetScreens.INBOX);
queryClient.invalidateQueries(ChatKeys.chatMessages(chatId));
queryClient.invalidateQueries(ChatKeys.chatSearch());
},