Merge remote-tracking branch 'origin/chats' into chats
This commit is contained in:
commit
0576565c83
12 changed files with 153 additions and 108 deletions
|
@ -1,6 +1,6 @@
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
import { getSettings } from 'soapbox/actions/settings';
|
||||||
import messages from 'soapbox/locales/messages';
|
import messages from 'soapbox/locales/messages';
|
||||||
import { queryClient } from 'soapbox/queries/client';
|
import { updatePageItem, appendPageItem } from 'soapbox/utils/queries';
|
||||||
import { play, soundCache } from 'soapbox/utils/sounds';
|
import { play, soundCache } from 'soapbox/utils/sounds';
|
||||||
|
|
||||||
import { connectStream } from '../stream';
|
import { connectStream } from '../stream';
|
||||||
|
@ -24,8 +24,6 @@ import {
|
||||||
processTimelineUpdate,
|
processTimelineUpdate,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
|
|
||||||
import type { InfiniteData } from '@tanstack/react-query';
|
|
||||||
import type { PaginatedResult } from 'soapbox/queries/chats';
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
import type { APIEntity, Chat, ChatMessage } from 'soapbox/types/entities';
|
import type { APIEntity, Chat, ChatMessage } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
@ -56,24 +54,10 @@ interface ChatPayload extends Omit<Chat, 'last_message'> {
|
||||||
const updateChat = (payload: ChatPayload) => {
|
const updateChat = (payload: ChatPayload) => {
|
||||||
const { last_message: lastMessage } = payload;
|
const { last_message: lastMessage } = payload;
|
||||||
|
|
||||||
queryClient.setQueriesData<InfiniteData<PaginatedResult<Chat>>>(['chats', 'search'], (data) => {
|
updatePageItem<Chat>(['chats', 'search'], payload as any, (o, n) => o.id === n.id);
|
||||||
if (data) {
|
|
||||||
const pages = data.pages.map(page => {
|
|
||||||
const result = page.result.map(chat => chat.id === payload.id ? payload as any : chat);
|
|
||||||
return { ...page, result };
|
|
||||||
});
|
|
||||||
return { ...data, pages };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (lastMessage) {
|
if (lastMessage) {
|
||||||
queryClient.setQueryData<InfiniteData<PaginatedResult<ChatMessage>>>(['chats', 'messages', payload.id], (data) => {
|
appendPageItem(['chats', 'messages', payload.id], lastMessage);
|
||||||
if (data) {
|
|
||||||
const pages = [...data.pages];
|
|
||||||
pages[0] = { ...pages[0], result: [...pages[0].result, lastMessage] };
|
|
||||||
return { ...data, pages };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, chatSilence, onC
|
||||||
weight='medium'
|
weight='medium'
|
||||||
theme='muted'
|
theme='muted'
|
||||||
truncate
|
truncate
|
||||||
className='w-full truncate-child pointer-events-none'
|
className='w-full h-5 truncate-child pointer-events-none'
|
||||||
data-testid='chat-last-message'
|
data-testid='chat-last-message'
|
||||||
dangerouslySetInnerHTML={{ __html: chat.last_message?.content }}
|
dangerouslySetInnerHTML={{ __html: chat.last_message?.content }}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,7 +21,10 @@ const ChatPage = () => {
|
||||||
const { top } = containerRef.current.getBoundingClientRect();
|
const { top } = containerRef.current.getBoundingClientRect();
|
||||||
const fullHeight = document.body.offsetHeight;
|
const fullHeight = document.body.offsetHeight;
|
||||||
|
|
||||||
setHeight(fullHeight - top);
|
// On mobile, account for bottom navigation.
|
||||||
|
const offset = document.body.clientWidth < 976 ? -61 : 0;
|
||||||
|
|
||||||
|
setHeight(fullHeight - top + offset);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,62 +1,55 @@
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import React, { useState } from 'react';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import React from 'react';
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import snackbar from 'soapbox/actions/snackbar';
|
import { CardTitle, HStack, IconButton, Stack } from 'soapbox/components/ui';
|
||||||
import AccountSearch from 'soapbox/components/account_search';
|
|
||||||
import { CardTitle, Stack } from 'soapbox/components/ui';
|
|
||||||
import { useChatContext } from 'soapbox/contexts/chat-context';
|
import { useChatContext } from 'soapbox/contexts/chat-context';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useDebounce, useFeatures } from 'soapbox/hooks';
|
||||||
import { IChat, useChats } from 'soapbox/queries/chats';
|
import { IChat } from 'soapbox/queries/chats';
|
||||||
import { queryClient } from 'soapbox/queries/client';
|
|
||||||
|
|
||||||
import ChatList from '../../chat-list';
|
import ChatList from '../../chat-list';
|
||||||
|
import ChatSearchInput from '../../chat-search-input';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.chats', defaultMessage: 'Messages' },
|
title: { id: 'column.chats', defaultMessage: 'Messages' },
|
||||||
searchPlaceholder: { id: 'chats.search_placeholder', defaultMessage: 'Start a chat with…' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const ChatPageSidebar = () => {
|
const ChatPageSidebar = () => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
const { setChat } = useChatContext();
|
const { setChat } = useChatContext();
|
||||||
const { getOrCreateChatByAccountId } = useChats();
|
|
||||||
|
|
||||||
const handleSuggestion = (accountId: string) => {
|
const debouncedSearch = useDebounce(search, 300);
|
||||||
handleClickOnSearchResult.mutate(accountId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickOnSearchResult = useMutation((accountId: string) => {
|
|
||||||
return getOrCreateChatByAccountId(accountId);
|
|
||||||
}, {
|
|
||||||
onError: (error: AxiosError) => {
|
|
||||||
const data = error.response?.data as any;
|
|
||||||
dispatch(snackbar.error(data?.error));
|
|
||||||
},
|
|
||||||
onSuccess: (response) => {
|
|
||||||
setChat(response.data);
|
|
||||||
queryClient.invalidateQueries(['chats', 'search']);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClickChat = (chat: IChat) => setChat(chat);
|
const handleClickChat = (chat: IChat) => setChat(chat);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack space={4} className='h-full'>
|
<Stack space={4} className='h-full'>
|
||||||
<Stack space={4} className='px-4 pt-4'>
|
<Stack space={4} className='px-4 pt-4'>
|
||||||
|
<HStack alignItems='center' justifyContent='between'>
|
||||||
<CardTitle title={intl.formatMessage(messages.title)} />
|
<CardTitle title={intl.formatMessage(messages.title)} />
|
||||||
|
|
||||||
<AccountSearch
|
<IconButton
|
||||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
src={require('@tabler/icons/edit.svg')}
|
||||||
onSelected={handleSuggestion}
|
iconClassName='w-5 h-5 text-gray-600'
|
||||||
/>
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{features.chatsSearch && (
|
||||||
|
<ChatSearchInput
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
onClear={() => setSearch('')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack className='flex-grow h-full'>
|
<Stack className='flex-grow h-full'>
|
||||||
<ChatList onClickChat={handleClickChat} />
|
<ChatList
|
||||||
|
onClickChat={handleClickChat}
|
||||||
|
searchValue={debouncedSearch}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import sumBy from 'lodash/sumBy';
|
import sumBy from 'lodash/sumBy';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { Icon, Input, Stack } from 'soapbox/components/ui';
|
import { Stack } from 'soapbox/components/ui';
|
||||||
import { useChatContext } from 'soapbox/contexts/chat-context';
|
import { useChatContext } from 'soapbox/contexts/chat-context';
|
||||||
import { useDebounce } from 'soapbox/hooks';
|
import { useDebounce, useFeatures } from 'soapbox/hooks';
|
||||||
import { IChat, useChats } from 'soapbox/queries/chats';
|
import { IChat, useChats } from 'soapbox/queries/chats';
|
||||||
|
|
||||||
import ChatList from '../chat-list';
|
import ChatList from '../chat-list';
|
||||||
import ChatPaneHeader from '../chat-pane-header';
|
import ChatPaneHeader from '../chat-pane-header';
|
||||||
|
import ChatSearchInput from '../chat-search-input';
|
||||||
import ChatSearch from '../chat-search/chat-search';
|
import ChatSearch from '../chat-search/chat-search';
|
||||||
import EmptyResultsBlankslate from '../chat-search/empty-results-blankslate';
|
import EmptyResultsBlankslate from '../chat-search/empty-results-blankslate';
|
||||||
import ChatWindow from '../chat-window';
|
import ChatWindow from '../chat-window';
|
||||||
|
@ -16,12 +16,8 @@ import { Pane } from '../ui';
|
||||||
|
|
||||||
import Blankslate from './blankslate';
|
import Blankslate from './blankslate';
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
searchPlaceholder: { id: 'chats.search_placeholder', defaultMessage: 'Search inbox' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const ChatPane = () => {
|
const ChatPane = () => {
|
||||||
const intl = useIntl();
|
const features = useFeatures();
|
||||||
const debounce = useDebounce;
|
const debounce = useDebounce;
|
||||||
|
|
||||||
const [value, setValue] = useState<string>();
|
const [value, setValue] = useState<string>();
|
||||||
|
@ -49,26 +45,15 @@ const ChatPane = () => {
|
||||||
if (hasSearchValue || Number(chats?.length) > 0) {
|
if (hasSearchValue || Number(chats?.length) > 0) {
|
||||||
return (
|
return (
|
||||||
<Stack space={4} className='flex-grow h-full'>
|
<Stack space={4} className='flex-grow h-full'>
|
||||||
|
{features.chatsSearch && (
|
||||||
<div className='px-4'>
|
<div className='px-4'>
|
||||||
<Input
|
<ChatSearchInput
|
||||||
type='text'
|
|
||||||
autoFocus
|
|
||||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
|
||||||
className='rounded-full'
|
|
||||||
value={value || ''}
|
value={value || ''}
|
||||||
onChange={(event) => setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
isSearch
|
onClear={clearValue}
|
||||||
append={
|
|
||||||
<button onClick={clearValue}>
|
|
||||||
<Icon
|
|
||||||
src={hasSearchValue ? require('@tabler/icons/x.svg') : require('@tabler/icons/search.svg')}
|
|
||||||
className='h-4 w-4 text-gray-700 dark:text-gray-600'
|
|
||||||
aria-hidden='true'
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{Number(chats?.length) > 0 ? (
|
{Number(chats?.length) > 0 ? (
|
||||||
<ChatList
|
<ChatList
|
||||||
|
|
45
app/soapbox/features/chats/components/chat-search-input.tsx
Normal file
45
app/soapbox/features/chats/components/chat-search-input.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { Icon, Input } from 'soapbox/components/ui';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
searchPlaceholder: { id: 'chats.search_placeholder', defaultMessage: 'Search inbox' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IChatSearchInput {
|
||||||
|
/** Search term. */
|
||||||
|
value: string,
|
||||||
|
/** Callback when the search value changes. */
|
||||||
|
onChange: React.ChangeEventHandler<HTMLInputElement>,
|
||||||
|
/** Callback when the input is cleared. */
|
||||||
|
onClear: React.MouseEventHandler<HTMLButtonElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Search input for filtering chats. */
|
||||||
|
const ChatSearchInput: React.FC<IChatSearchInput> = ({ value, onChange, onClear }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type='text'
|
||||||
|
autoFocus
|
||||||
|
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||||
|
className='rounded-full'
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
isSearch
|
||||||
|
append={
|
||||||
|
<button onClick={onClear}>
|
||||||
|
<Icon
|
||||||
|
src={value.length ? require('@tabler/icons/x.svg') : require('@tabler/icons/search.svg')}
|
||||||
|
className='h-4 w-4 text-gray-700 dark:text-gray-600'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatSearchInput;
|
|
@ -1,26 +1,16 @@
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { connectDirectStream } from 'soapbox/actions/streaming';
|
|
||||||
import { ChatProvider } from 'soapbox/contexts/chat-context';
|
import { ChatProvider } from 'soapbox/contexts/chat-context';
|
||||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
import { useOwnAccount } from 'soapbox/hooks';
|
||||||
|
|
||||||
import ChatPane from './chat-pane/chat-pane';
|
import ChatPane from './chat-pane/chat-pane';
|
||||||
|
|
||||||
const ChatWidget = () => {
|
const ChatWidget = () => {
|
||||||
const account = useOwnAccount();
|
const account = useOwnAccount();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
const shouldHideWidget = Boolean(path.match(/^\/chats/));
|
const shouldHideWidget = Boolean(path.match(/^\/chats/));
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const disconnect = dispatch(connectDirectStream());
|
|
||||||
|
|
||||||
return (() => {
|
|
||||||
disconnect();
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!account?.chats_onboarded || shouldHideWidget) {
|
if (!account?.chats_onboarded || shouldHideWidget) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const AccountRecord = ImmutableRecord({
|
||||||
avatar_static: '',
|
avatar_static: '',
|
||||||
birthday: '',
|
birthday: '',
|
||||||
bot: false,
|
bot: false,
|
||||||
chats_onboarded: false,
|
chats_onboarded: true,
|
||||||
created_at: '',
|
created_at: '',
|
||||||
discoverable: false,
|
discoverable: false,
|
||||||
display_name: '',
|
display_name: '',
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
/** Custom layout for chats on desktop. */
|
/** Custom layout for chats on desktop. */
|
||||||
const ChatsPage: React.FC = ({ children }) => {
|
const ChatsPage: React.FC = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<div className='md:col-span-12 lg:col-span-9 pb-16 sm:pb-0'>
|
<div className='md:col-span-12 lg:col-span-9'>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,8 +6,9 @@ import snackbar from 'soapbox/actions/snackbar';
|
||||||
import { getNextLink } from 'soapbox/api';
|
import { getNextLink } from 'soapbox/api';
|
||||||
import compareId from 'soapbox/compare_id';
|
import compareId from 'soapbox/compare_id';
|
||||||
import { useChatContext } from 'soapbox/contexts/chat-context';
|
import { useChatContext } from 'soapbox/contexts/chat-context';
|
||||||
import { useApi, useAppDispatch } from 'soapbox/hooks';
|
import { useApi, useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||||
import { normalizeChatMessage } from 'soapbox/normalizers';
|
import { normalizeChatMessage } from 'soapbox/normalizers';
|
||||||
|
import { flattenPages, updatePageItem } from 'soapbox/utils/queries';
|
||||||
|
|
||||||
import { queryClient } from './client';
|
import { queryClient } from './client';
|
||||||
|
|
||||||
|
@ -88,10 +89,7 @@ const useChatMessages = (chatId: string) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = queryInfo.data?.pages.reduce<IChatMessage[]>(
|
const data = flattenPages(queryInfo);
|
||||||
(prev: IChatMessage[], curr) => [...curr.result, ...prev],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...queryInfo,
|
...queryInfo,
|
||||||
|
@ -102,10 +100,12 @@ const useChatMessages = (chatId: string) => {
|
||||||
const useChats = (search?: string) => {
|
const useChats = (search?: string) => {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
const getChats = async(pageParam?: any): Promise<PaginatedResult<IChat>> => {
|
const getChats = async(pageParam?: any): Promise<PaginatedResult<IChat>> => {
|
||||||
|
const endpoint = features.chatsV2 ? '/api/v2/pleroma/chats' : '/api/v1/pleroma/chats';
|
||||||
const nextPageLink = pageParam?.link;
|
const nextPageLink = pageParam?.link;
|
||||||
const uri = nextPageLink || '/api/v1/pleroma/chats';
|
const uri = nextPageLink || endpoint;
|
||||||
const response = await api.get<IChat[]>(uri, {
|
const response = await api.get<IChat[]>(uri, {
|
||||||
params: {
|
params: {
|
||||||
search,
|
search,
|
||||||
|
@ -137,10 +137,7 @@ const useChats = (search?: string) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = queryInfo.data?.pages.reduce<IChat[]>(
|
const data = flattenPages(queryInfo);
|
||||||
(prev: IChat[], curr) => [...prev, ...curr.result],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const chatsQuery = {
|
const chatsQuery = {
|
||||||
...queryInfo,
|
...queryInfo,
|
||||||
|
@ -158,7 +155,7 @@ const useChat = (chatId: string) => {
|
||||||
|
|
||||||
const markChatAsRead = (lastReadId: string) => {
|
const markChatAsRead = (lastReadId: string) => {
|
||||||
api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId })
|
api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId })
|
||||||
.then(() => queryClient.invalidateQueries(['chats', 'search']))
|
.then(({ data }) => updatePageItem(['chats', 'search'], data, (o, n) => o.id === n.id))
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -204,6 +204,12 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
chats: v.software === TRUTHSOCIAL || (v.software === PLEROMA && gte(v.version, '2.1.0')),
|
chats: v.software === TRUTHSOCIAL || (v.software === PLEROMA && gte(v.version, '2.1.0')),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ability to search among chats.
|
||||||
|
* @see GET /api/v1/pleroma/chats
|
||||||
|
*/
|
||||||
|
chatsSearch: v.software === TRUTHSOCIAL,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paginated chats API.
|
* Paginated chats API.
|
||||||
* @see GET /api/v2/chats
|
* @see GET /api/v2/chats
|
||||||
|
|
42
app/soapbox/utils/queries.ts
Normal file
42
app/soapbox/utils/queries.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { queryClient } from 'soapbox/queries/client';
|
||||||
|
|
||||||
|
import type { InfiniteData, QueryKey, UseInfiniteQueryResult } from '@tanstack/react-query';
|
||||||
|
import type { PaginatedResult } from 'soapbox/queries/chats';
|
||||||
|
|
||||||
|
/** Flatten paginated results into a single array. */
|
||||||
|
const flattenPages = <T>(queryInfo: UseInfiniteQueryResult<PaginatedResult<T>>) => {
|
||||||
|
return queryInfo.data?.pages.reduce<T[]>(
|
||||||
|
(prev: T[], curr) => [...prev, ...curr.result],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Traverse pages and update the item inside if found. */
|
||||||
|
const updatePageItem = <T>(queryKey: QueryKey, newItem: T, isItem: (item: T, newItem: T) => boolean) => {
|
||||||
|
queryClient.setQueriesData<InfiniteData<PaginatedResult<T>>>(queryKey, (data) => {
|
||||||
|
if (data) {
|
||||||
|
const pages = data.pages.map(page => {
|
||||||
|
const result = page.result.map(item => isItem(item, newItem) ? newItem : item);
|
||||||
|
return { ...page, result };
|
||||||
|
});
|
||||||
|
return { ...data, pages };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Insert the new item at the beginning of the first page. */
|
||||||
|
const appendPageItem = <T>(queryKey: QueryKey, newItem: T) => {
|
||||||
|
queryClient.setQueryData<InfiniteData<PaginatedResult<T>>>(queryKey, (data) => {
|
||||||
|
if (data) {
|
||||||
|
const pages = [...data.pages];
|
||||||
|
pages[0] = { ...pages[0], result: [...pages[0].result, newItem] };
|
||||||
|
return { ...data, pages };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
flattenPages,
|
||||||
|
updatePageItem,
|
||||||
|
appendPageItem,
|
||||||
|
};
|
Loading…
Reference in a new issue