bigbuffet-rw/app/soapbox/queries/chats.ts

376 lines
12 KiB
TypeScript
Raw Normal View History

import { InfiniteData, useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query';
import sumBy from 'lodash/sumBy';
2022-08-10 05:38:49 -07:00
2022-09-29 10:36:35 -07:00
import { importFetchedAccount, importFetchedAccounts } from 'soapbox/actions/importer';
2022-09-15 07:49:09 -07:00
import { getNextLink } from 'soapbox/api';
2022-11-02 12:28:16 -07:00
import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context';
import { useStatContext } from 'soapbox/contexts/stat-context';
2022-11-09 09:24:44 -08:00
import { useApi, useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
import { normalizeChatMessage } from 'soapbox/normalizers';
2022-12-20 07:47:46 -08:00
import toast from 'soapbox/toast';
import { ChatMessage } from 'soapbox/types/entities';
import { reOrderChatListItems, updateChatMessage } from 'soapbox/utils/chats';
import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries';
2022-08-10 05:38:49 -07:00
2022-08-16 10:38:17 -07:00
import { queryClient } from './client';
import { useFetchRelationships } from './relationships';
2022-08-16 10:38:17 -07:00
2023-06-20 12:24:39 -07:00
import type { Account } from 'soapbox/schemas';
2022-09-08 09:47:19 -07:00
2022-12-05 05:39:28 -08:00
export const messageExpirationOptions = [604800, 1209600, 2592000, 7776000];
2022-10-26 10:28:50 -07:00
export enum MessageExpirationValues {
2022-12-07 07:44:16 -08:00
'SEVEN' = messageExpirationOptions[0],
'FOURTEEN' = messageExpirationOptions[1],
'THIRTY' = messageExpirationOptions[2],
'NINETY' = messageExpirationOptions[3]
}
2022-08-10 05:38:49 -07:00
export interface IChat {
accepted: boolean
2023-06-20 12:24:39 -07:00
account: Account
chat_type: 'channel' | 'direct'
2022-10-28 10:01:39 -07:00
created_at: string
2022-08-16 10:38:17 -07:00
created_by_account: string
discarded_at: null | string
id: string
2022-08-26 09:41:25 -07:00
last_message: null | {
account_id: string
chat_id: string
content: string
created_at: string
discarded_at: string | null
id: string
unread: boolean
}
latest_read_message_by_account?: {
id: string
2022-11-02 12:28:16 -07:00
date: string
}[]
2022-10-28 10:01:39 -07:00
latest_read_message_created_at: null | string
message_expiration?: MessageExpirationValues
unread: number
2022-08-10 05:38:49 -07:00
}
type UpdateChatVariables = {
message_expiration: MessageExpirationValues
}
type CreateReactionVariables = {
messageId: string
emoji: string
chatMessage?: ChatMessage
}
const ChatKeys = {
chat: (chatId?: string) => ['chats', 'chat', chatId] as const,
2022-09-27 12:42:24 -07:00
chatMessages: (chatId: string) => ['chats', 'messages', chatId] as const,
chatSearch: (searchQuery?: string) => searchQuery ? ['chats', 'search', searchQuery] : ['chats', 'search'] as const,
2022-09-27 12:42:24 -07:00
};
/** Check if item is most recent */
const isLastMessage = (chatMessageId: string): boolean => {
const queryData = queryClient.getQueryData<InfiniteData<PaginatedResult<IChat>>>(ChatKeys.chatSearch());
const items = flattenPages<IChat>(queryData);
const chat = items?.find((item) => item.last_message?.id === chatMessageId);
return !!chat;
};
2022-10-25 10:10:53 -07:00
const useChatMessages = (chat: IChat) => {
2022-08-10 05:38:49 -07:00
const api = useApi();
2022-10-25 10:10:53 -07:00
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat.account.id, 'blocked_by']));
2022-08-10 05:38:49 -07:00
const getChatMessages = async (chatId: string, pageParam?: any): Promise<PaginatedResult<ChatMessage>> => {
2022-09-15 07:49:09 -07:00
const nextPageLink = pageParam?.link;
const uri = nextPageLink || `/api/v1/pleroma/chats/${chatId}/messages`;
2022-12-06 13:12:24 -08:00
const response = await api.get<any[]>(uri);
2022-09-15 07:49:09 -07:00
const { data } = response;
2022-08-10 05:38:49 -07:00
2022-09-15 07:49:09 -07:00
const link = getNextLink(response);
const hasMore = !!link;
2022-12-06 13:12:24 -08:00
const result = data.map(normalizeChatMessage);
2022-08-10 05:38:49 -07:00
return {
result,
2022-09-15 07:49:09 -07:00
link,
2022-08-10 05:38:49 -07:00
hasMore,
};
};
2022-10-25 10:10:53 -07:00
const queryInfo = useInfiniteQuery(ChatKeys.chatMessages(chat.id), ({ pageParam }) => getChatMessages(chat.id, pageParam), {
enabled: !isBlocked,
2022-11-03 10:55:33 -07:00
cacheTime: 0,
staleTime: 0,
2022-08-10 05:38:49 -07:00
getNextPageParam: (config) => {
if (config.hasMore) {
2022-09-15 07:49:09 -07:00
return { link: config.link };
2022-08-10 05:38:49 -07:00
}
return undefined;
},
});
2022-12-06 13:12:24 -08:00
const data = flattenPages(queryInfo.data)?.reverse();
2022-08-10 05:38:49 -07:00
return {
...queryInfo,
data,
};
};
2022-08-26 09:41:25 -07:00
2022-09-09 07:24:25 -07:00
const useChats = (search?: string) => {
2022-08-10 05:38:49 -07:00
const api = useApi();
2022-09-08 09:47:19 -07:00
const dispatch = useAppDispatch();
2022-09-22 14:04:52 -07:00
const features = useFeatures();
const { setUnreadChatsCount } = useStatContext();
const fetchRelationships = useFetchRelationships();
2022-08-10 05:38:49 -07:00
const getChats = async (pageParam?: any): Promise<PaginatedResult<IChat>> => {
2022-09-22 14:04:52 -07:00
const endpoint = features.chatsV2 ? '/api/v2/pleroma/chats' : '/api/v1/pleroma/chats';
2022-09-15 07:49:09 -07:00
const nextPageLink = pageParam?.link;
2022-09-22 14:04:52 -07:00
const uri = nextPageLink || endpoint;
2022-09-15 07:49:09 -07:00
const response = await api.get<IChat[]>(uri, {
2022-12-05 16:38:32 -08:00
params: search ? {
2022-09-09 07:24:25 -07:00
search,
2022-12-05 16:38:32 -08:00
} : undefined,
2022-08-26 09:41:25 -07:00
});
2022-09-15 07:49:09 -07:00
const { data } = response;
2022-08-26 09:41:25 -07:00
2022-09-15 07:49:09 -07:00
const link = getNextLink(response);
const hasMore = !!link;
2022-08-26 09:41:25 -07:00
setUnreadChatsCount(Number(response.headers['x-unread-messages-count']) || sumBy(data, (chat) => chat.unread));
2022-09-08 09:47:19 -07:00
// Set the relationships to these users in the redux store.
fetchRelationships.mutate({ accountIds: data.map((item) => item.account.id) });
2022-09-29 10:36:35 -07:00
dispatch(importFetchedAccounts(data.map((item) => item.account)));
2022-09-08 09:47:19 -07:00
2022-08-26 09:41:25 -07:00
return {
result: data,
hasMore,
2022-09-15 07:49:09 -07:00
link,
2022-08-26 09:41:25 -07:00
};
2022-08-10 05:38:49 -07:00
};
const queryInfo = useInfiniteQuery(ChatKeys.chatSearch(search), ({ pageParam }) => getChats(pageParam), {
2022-08-26 09:41:25 -07:00
keepPreviousData: true,
enabled: features.chats,
2022-08-26 09:41:25 -07:00
getNextPageParam: (config) => {
if (config.hasMore) {
2022-09-15 07:49:09 -07:00
return { link: config.link };
2022-08-26 09:41:25 -07:00
}
return undefined;
},
2022-08-10 05:38:49 -07:00
});
2022-12-06 13:12:24 -08:00
const data = flattenPages(queryInfo.data);
2022-08-26 09:41:25 -07:00
const chatsQuery = {
...queryInfo,
data,
};
2022-08-10 05:38:49 -07:00
const getOrCreateChatByAccountId = (accountId: string) => api.post<IChat>(`/api/v1/pleroma/chats/by-account-id/${accountId}`);
return { chatsQuery, getOrCreateChatByAccountId };
};
const useChat = (chatId?: string) => {
const api = useApi();
2022-09-29 10:36:35 -07:00
const dispatch = useAppDispatch();
const fetchRelationships = useFetchRelationships();
const getChat = async () => {
if (chatId) {
const { data } = await api.get<IChat>(`/api/v1/pleroma/chats/${chatId}`);
2022-09-29 10:36:35 -07:00
fetchRelationships.mutate({ accountIds: [data.account.id] });
2022-09-29 10:36:35 -07:00
dispatch(importFetchedAccount(data.account));
return data;
}
};
2022-11-02 12:28:16 -07:00
return useQuery<IChat | undefined>(ChatKeys.chat(chatId), getChat, {
2022-11-07 12:40:19 -08:00
cacheTime: 0,
2022-11-02 12:28:16 -07:00
enabled: !!chatId,
});
};
2022-09-28 13:20:59 -07:00
const useChatActions = (chatId: string) => {
2023-06-25 10:35:09 -07:00
const { account } = useOwnAccount();
2022-08-10 05:38:49 -07:00
const api = useApi();
2022-12-20 07:47:46 -08:00
// const dispatch = useAppDispatch();
2022-11-09 09:24:44 -08:00
const { setUnreadChatsCount } = useStatContext();
2022-11-02 12:28:16 -07:00
const { chat, changeScreen } = useChatContext();
2022-08-10 05:38:49 -07:00
2022-10-28 10:01:39 -07:00
const markChatAsRead = async (lastReadId: string) => {
return api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId })
.then(({ data }) => {
updatePageItem(ChatKeys.chatSearch(), data, (o, n) => o.id === n.id);
const queryData = queryClient.getQueryData<InfiniteData<PaginatedResult<unknown>>>(ChatKeys.chatSearch());
2022-10-28 10:01:39 -07:00
if (queryData) {
const flattenedQueryData: any = flattenPages(queryData)?.map((chat: any) => {
if (chat.id === data.id) {
return data;
} else {
return chat;
}
});
setUnreadChatsCount(sumBy(flattenedQueryData, (chat: IChat) => chat.unread));
}
2022-10-28 10:01:39 -07:00
return data;
})
2022-08-30 07:10:31 -07:00
.catch(() => null);
};
2022-08-10 05:38:49 -07:00
2022-11-09 09:24:44 -08:00
const createChatMessage = useMutation(
2023-02-08 10:24:41 -08:00
({ chatId, content, mediaIds }: { chatId: string, content: string, mediaIds?: string[] }) => {
return api.post<ChatMessage>(`/api/v1/pleroma/chats/${chatId}/messages`, {
2023-02-08 10:24:41 -08:00
content,
media_id: (mediaIds && mediaIds.length === 1) ? mediaIds[0] : undefined, // Pleroma backwards-compat
media_ids: mediaIds,
});
},
2022-11-09 09:24:44 -08:00
{
retry: false,
onMutate: async (variables) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(['chats', 'messages', variables.chatId]);
// Snapshot the previous value
const prevContent = variables.content;
const prevChatMessages = queryClient.getQueryData(['chats', 'messages', variables.chatId]);
const pendingId = String(Number(new Date()));
2022-11-09 09:24:44 -08:00
// Optimistically update to the new value
queryClient.setQueryData(ChatKeys.chatMessages(variables.chatId), (prevResult: any) => {
const newResult = { ...prevResult };
newResult.pages = newResult.pages.map((page: any, idx: number) => {
if (idx === 0) {
return {
...page,
2022-11-09 10:17:10 -08:00
result: [
normalizeChatMessage({
content: variables.content,
id: pendingId,
2022-11-09 10:17:10 -08:00
created_at: new Date(),
account_id: account?.id,
pending: true,
unread: true,
}),
2022-12-07 14:18:03 -08:00
...page.result,
2022-11-09 10:17:10 -08:00
],
2022-11-09 09:24:44 -08:00
};
}
return page;
});
return newResult;
});
return { prevChatMessages, prevContent, pendingId };
2022-11-09 09:24:44 -08:00
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (_error: any, variables, context: any) => {
queryClient.setQueryData(['chats', 'messages', variables.chatId], context.prevChatMessages);
},
onSuccess: (response: any, variables, context) => {
const nextChat = { ...chat, last_message: response.data };
updatePageItem(ChatKeys.chatSearch(), nextChat, (o, n) => o.id === n.id);
updatePageItem(
ChatKeys.chatMessages(variables.chatId),
normalizeChatMessage(response.data),
(o) => o.id === context.pendingId,
);
reOrderChatListItems();
2022-11-09 09:24:44 -08:00
},
},
);
2022-08-10 05:38:49 -07:00
const updateChat = useMutation((data: UpdateChatVariables) => api.patch<IChat>(`/api/v1/pleroma/chats/${chatId}`, data), {
onMutate: async (data) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(ChatKeys.chat(chatId));
// Snapshot the previous value
const prevChat = { ...chat };
const nextChat = { ...chat, ...data };
// Optimistically update to the new value
queryClient.setQueryData(ChatKeys.chat(chatId), nextChat);
2022-11-02 12:28:16 -07:00
// 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) => {
2022-11-02 12:28:16 -07:00
changeScreen(ChatWidgetScreens.CHAT, context.prevChat.id);
queryClient.setQueryData(ChatKeys.chat(chatId), context.prevChat);
2022-12-20 07:47:46 -08:00
toast.error('Chat Settings failed to update.');
},
onSuccess() {
queryClient.invalidateQueries(ChatKeys.chat(chatId));
2022-10-26 10:28:50 -07:00
queryClient.invalidateQueries(ChatKeys.chatSearch());
2022-12-20 07:47:46 -08:00
toast.success('Chat Settings updated successfully');
},
});
2022-08-16 05:39:58 -07:00
const deleteChatMessage = (chatMessageId: string) => api.delete<IChat>(`/api/v1/pleroma/chats/${chatId}/messages/${chatMessageId}`);
2022-08-16 10:38:17 -07:00
const acceptChat = useMutation(() => api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/accept`), {
onSuccess(response) {
2022-11-02 12:28:16 -07:00
changeScreen(ChatWidgetScreens.CHAT, response.data.id);
queryClient.invalidateQueries(ChatKeys.chat(chatId));
queryClient.invalidateQueries(ChatKeys.chatMessages(chatId));
queryClient.invalidateQueries(ChatKeys.chatSearch());
2022-08-16 10:38:17 -07:00
},
});
2022-08-16 10:54:36 -07:00
const deleteChat = useMutation(() => api.delete<IChat>(`/api/v1/pleroma/chats/${chatId}`), {
2022-11-02 12:28:16 -07:00
onSuccess() {
changeScreen(ChatWidgetScreens.INBOX);
queryClient.invalidateQueries(ChatKeys.chatMessages(chatId));
queryClient.invalidateQueries(ChatKeys.chatSearch());
2022-08-16 10:54:36 -07:00
},
});
const createReaction = useMutation((data: CreateReactionVariables) => api.post(`/api/v1/pleroma/chats/${chatId}/messages/${data.messageId}/reactions`, {
emoji: data.emoji,
}), {
// TODO: add optimistic updates
onSuccess(response) {
updateChatMessage(response.data);
},
});
const deleteReaction = useMutation(
(data: CreateReactionVariables) => api.delete(`/api/v1/pleroma/chats/${chatId}/messages/${data.messageId}/reactions/${data.emoji}`),
{
onSuccess() {
queryClient.invalidateQueries(ChatKeys.chatMessages(chatId));
},
},
);
return {
acceptChat,
createChatMessage,
createReaction,
deleteChat,
deleteChatMessage,
deleteReaction,
markChatAsRead,
updateChat,
};
2022-08-10 05:38:49 -07:00
};
export { ChatKeys, useChat, useChatActions, useChats, useChatMessages, isLastMessage };