kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge remote-tracking branch 'origin/chats' into chats-router
commit
44e7b5f831
|
@ -277,6 +277,7 @@ module.exports = {
|
|||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
'no-undef': 'off', // https://stackoverflow.com/a/69155899
|
||||
'space-before-function-paren': 'off',
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
},
|
||||
|
|
|
@ -3,6 +3,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
|
||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||
import ComposeButton from 'soapbox/features/ui/components/compose-button';
|
||||
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
@ -24,12 +25,12 @@ const messages = defineMessages({
|
|||
/** Desktop sidebar with links to different views in the app. */
|
||||
const SidebarNavigation = () => {
|
||||
const intl = useIntl();
|
||||
const { unreadChatsCount } = useStatContext();
|
||||
|
||||
const instance = useAppSelector((state) => state.instance);
|
||||
const settings = useAppSelector((state) => getSettings(state));
|
||||
const account = useOwnAccount();
|
||||
const notificationCount = useAppSelector((state) => state.notifications.get('unread'));
|
||||
const chatsCount = useAppSelector((state) => state.chats.items.reduce((acc, curr) => acc + Math.min(curr.unread || 0, 1), 0));
|
||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
||||
|
||||
|
@ -114,7 +115,7 @@ const SidebarNavigation = () => {
|
|||
<SidebarNavigationLink
|
||||
to='/chats'
|
||||
icon={require('@tabler/icons/mail.svg')}
|
||||
count={chatsCount}
|
||||
count={unreadChatsCount}
|
||||
text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -2,13 +2,15 @@ import React from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ThumbNavigationLink from 'soapbox/components/thumb_navigation-link';
|
||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
const ThumbNavigation: React.FC = (): JSX.Element => {
|
||||
const account = useOwnAccount();
|
||||
const { unreadChatsCount } = useStatContext();
|
||||
|
||||
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
||||
const chatsCount = useAppSelector((state) => state.chats.items.reduce((acc, curr) => acc + Math.min(curr.unread || 0, 1), 0));
|
||||
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
||||
const features = getFeatures(useAppSelector((state) => state.instance));
|
||||
|
||||
|
@ -21,7 +23,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
|
|||
text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />}
|
||||
to='/chats'
|
||||
exact
|
||||
count={chatsCount}
|
||||
count={unreadChatsCount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ const Widget: React.FC<IWidget> = ({
|
|||
action,
|
||||
}): JSX.Element => {
|
||||
return (
|
||||
<Stack space={2}>
|
||||
<HStack alignItems='center'>
|
||||
<Stack space={4}>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<WidgetTitle title={title} />
|
||||
{action || (onActionClick && (
|
||||
<IconButton
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||
|
||||
type IStatContext = {
|
||||
unreadChatsCount: number,
|
||||
setUnreadChatsCount: React.Dispatch<React.SetStateAction<number>>
|
||||
}
|
||||
|
||||
const StatContext = createContext<any>({
|
||||
unreadChatsCount: 0,
|
||||
});
|
||||
|
||||
const StatProvider: React.FC = ({ children }) => {
|
||||
const [unreadChatsCount, setUnreadChatsCount] = useState<number>(0);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
unreadChatsCount,
|
||||
setUnreadChatsCount,
|
||||
}), [unreadChatsCount]);
|
||||
|
||||
return (
|
||||
<StatContext.Provider value={value}>
|
||||
{children}
|
||||
</StatContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useStatContext = (): IStatContext => useContext(StatContext);
|
||||
|
||||
export { StatProvider, useStatContext };
|
|
@ -6,6 +6,7 @@ import { Stack, HStack, Card, Avatar, Text, Icon } from 'soapbox/components/ui';
|
|||
import IconButton from 'soapbox/components/ui/icon-button/icon-button';
|
||||
import StatusCard from 'soapbox/features/status/components/card';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { adKeys } from 'soapbox/queries/ads';
|
||||
|
||||
import type { Card as CardEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -29,7 +30,7 @@ const Ad: React.FC<IAd> = ({ card, impression, expires }) => {
|
|||
|
||||
/** Invalidate query cache for ads. */
|
||||
const bustCache = (): void => {
|
||||
queryClient.invalidateQueries(['ads']);
|
||||
queryClient.invalidateQueries(adKeys.ads);
|
||||
};
|
||||
|
||||
/** Toggle the info box on click. */
|
||||
|
|
|
@ -1,10 +1,24 @@
|
|||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import RelativeTimestamp from 'soapbox/components/relative-timestamp';
|
||||
import { Avatar, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import VerificationBadge from 'soapbox/components/verification_badge';
|
||||
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { IChat, IChatSilence, useChat, useChatSilence } from 'soapbox/queries/chats';
|
||||
|
||||
import type { IChat, IChatSilence } from 'soapbox/queries/chats';
|
||||
import type { Menu } from 'soapbox/components/dropdown_menu';
|
||||
|
||||
const messages = defineMessages({
|
||||
silenceNotifications: { id: 'chat_settings.silence_notifications', defaultMessage: 'Silence notifications' },
|
||||
unsilenceNotifications: { id: 'chat_settings.unsilence_notifications', defaultMessage: 'Unsilence notifications' },
|
||||
leaveMessage: { id: 'chat_settings.leave.message', defaultMessage: 'Are you sure you want to leave this chat? Messages will be deleted for you and this chat will be removed from your inbox.' },
|
||||
leaveHeading: { id: 'chat_settings.leave.heading', defaultMessage: 'Leave Chat' },
|
||||
leaveConfirm: { id: 'chat_settings.leave.confirm', defaultMessage: 'Leave Chat' },
|
||||
leaveChat: { id: 'chat_settings.options.leave_chat', defaultMessage: 'Leave Chat' },
|
||||
});
|
||||
|
||||
interface IChatListItemInterface {
|
||||
chat: IChat,
|
||||
|
@ -13,12 +27,60 @@ interface IChatListItemInterface {
|
|||
}
|
||||
|
||||
const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, chatSilence, onClick }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const { handleSilence } = useChatSilence(chat);
|
||||
const { deleteChat } = useChat(chat?.id as string);
|
||||
|
||||
const menu = useMemo((): Menu => {
|
||||
const menu: Menu = [];
|
||||
|
||||
if (chatSilence) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.unsilenceNotifications),
|
||||
action: (event) => {
|
||||
event.stopPropagation();
|
||||
handleSilence();
|
||||
},
|
||||
icon: require('@tabler/icons/bell.svg'),
|
||||
});
|
||||
} else {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.silenceNotifications),
|
||||
action: (event) => {
|
||||
event.stopPropagation();
|
||||
handleSilence();
|
||||
},
|
||||
icon: require('@tabler/icons/bell-off.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.leaveChat),
|
||||
action: (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.leaveHeading),
|
||||
message: intl.formatMessage(messages.leaveMessage),
|
||||
confirm: intl.formatMessage(messages.leaveConfirm),
|
||||
confirmationTheme: 'primary',
|
||||
onConfirm: () => deleteChat.mutate(),
|
||||
}));
|
||||
},
|
||||
icon: require('@tabler/icons/logout.svg'),
|
||||
});
|
||||
|
||||
return menu;
|
||||
}, [chatSilence]);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={chat.id}
|
||||
type='button'
|
||||
onClick={() => onClick(chat)}
|
||||
className='px-2 py-3 w-full flex flex-col rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 focus:shadow-inset-ring'
|
||||
className='group px-2 py-3 w-full flex flex-col rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 focus:shadow-inset-ring'
|
||||
data-testid='chat'
|
||||
>
|
||||
<HStack alignItems='center' justifyContent='between' space={2} className='w-full'>
|
||||
|
@ -36,7 +98,7 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, chatSilence, onC
|
|||
align='left'
|
||||
size='sm'
|
||||
weight='medium'
|
||||
theme='muted'
|
||||
theme={chat.last_message.unread ? 'default' : 'muted'}
|
||||
truncate
|
||||
className='w-full h-5 truncate-child pointer-events-none'
|
||||
data-testid='chat-last-message'
|
||||
|
@ -47,6 +109,16 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, chatSilence, onC
|
|||
</HStack>
|
||||
|
||||
<HStack alignItems='center' space={2}>
|
||||
<div className='text-gray-600 hidden group-hover:block hover:text-gray-100'>
|
||||
{/* TODO: fix nested buttons here */}
|
||||
<DropdownMenuContainer
|
||||
items={menu}
|
||||
src={require('@tabler/icons/dots.svg')}
|
||||
title='Settings'
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{chatSilence ? (
|
||||
<Icon src={require('@tabler/icons/bell-off.svg')} className='w-5 h-5 text-gray-600' />
|
||||
) : null}
|
||||
|
|
|
@ -14,11 +14,10 @@ import ChatListItem from './chat-list-item';
|
|||
interface IChatList {
|
||||
onClickChat: (chat: any) => void,
|
||||
useWindowScroll?: boolean,
|
||||
fade?: boolean,
|
||||
searchValue?: string
|
||||
}
|
||||
|
||||
const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false, searchValue, fade }) => {
|
||||
const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false, searchValue }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const chatListRef = useRef(null);
|
||||
|
@ -76,22 +75,20 @@ const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false, s
|
|||
)}
|
||||
</PullToRefresh>
|
||||
|
||||
{fade && (
|
||||
<>
|
||||
<div
|
||||
className={classNames('inset-x-0 top-0 flex rounded-t-lg justify-center bg-gradient-to-b from-white pb-12 pt-8 pointer-events-none dark:from-gray-900 absolute transition-opacity duration-500', {
|
||||
'opacity-0': isNearTop,
|
||||
'opacity-100': !isNearTop,
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={classNames('inset-x-0 bottom-0 flex rounded-b-lg justify-center bg-gradient-to-t from-white pt-12 pb-8 pointer-events-none dark:from-gray-900 absolute transition-opacity duration-500', {
|
||||
'opacity-0': isNearBottom,
|
||||
'opacity-100': !isNearBottom,
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<div
|
||||
className={classNames('inset-x-0 top-0 flex rounded-t-lg justify-center bg-gradient-to-b from-white to-transparent pb-12 pt-8 pointer-events-none dark:from-gray-900 absolute transition-opacity duration-500', {
|
||||
'opacity-0': isNearTop,
|
||||
'opacity-100': !isNearTop,
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={classNames('inset-x-0 bottom-0 flex rounded-b-lg justify-center bg-gradient-to-t from-white to-transparent pt-12 pb-8 pointer-events-none dark:from-gray-900 absolute transition-opacity duration-500', {
|
||||
'opacity-0': isNearBottom,
|
||||
'opacity-100': !isNearBottom,
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useChat } from 'soapbox/queries/chats';
|
|||
|
||||
const messages = defineMessages({
|
||||
leaveChatHeading: { id: 'chat_message_list_intro.leave_chat.heading', defaultMessage: 'Leave Chat' },
|
||||
leaveChatMessage: { id: 'chat_message_list_intro.leave_chat.message', defaultMessage: 'Are you sure you want to leave this chat? This conversation will be removed from your inbox.' },
|
||||
leaveChatMessage: { id: 'chat_message_list_intro.leave_chat.message', defaultMessage: 'Are you sure you want to leave this chat? Messages will be deleted for you and this chat will be removed from your inbox.' },
|
||||
leaveChatConfirm: { id: 'chat_message_list_intro.leave_chat.confirm', defaultMessage: 'Leave Chat' },
|
||||
intro: { id: 'chat_message_list_intro.intro', defaultMessage: 'wants to start a chat with you' },
|
||||
accept: { id: 'chat_message_list_intro.actions.accept', defaultMessage: 'Accept' },
|
||||
|
|
|
@ -14,7 +14,7 @@ import PlaceholderChatMessage from 'soapbox/features/placeholder/components/plac
|
|||
import Bundle from 'soapbox/features/ui/components/bundle';
|
||||
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import { IChat, IChatMessage, useChat, useChatMessages } from 'soapbox/queries/chats';
|
||||
import { chatKeys, IChat, IChatMessage, useChat, useChatMessages } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { onlyEmoji } from 'soapbox/utils/rich_content';
|
||||
|
||||
|
@ -30,6 +30,8 @@ const messages = defineMessages({
|
|||
more: { id: 'chats.actions.more', defaultMessage: 'More' },
|
||||
delete: { id: 'chats.actions.delete', defaultMessage: 'Delete for both' },
|
||||
copy: { id: 'chats.actions.copy', defaultMessage: 'Copy' },
|
||||
report: { id: 'chats.actions.report', defaultMessage: 'Report' },
|
||||
deleteForMe: { id: 'chats.actions.deleteForMe', defaultMessage: 'Delete for me' },
|
||||
blockedBy: { id: 'chat_message_list.blockedBy', defaultMessage: 'You are blocked by' },
|
||||
networkFailureTitle: { id: 'chat_message_list.network_failure.title', defaultMessage: 'Whoops!' },
|
||||
networkFailureSubtitle: { id: 'chat_message_list.network_failure.subtitle', defaultMessage: 'We encountered a network failure.' },
|
||||
|
@ -94,7 +96,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
|
|||
|
||||
const handleDeleteMessage = useMutation((chatMessageId: string) => deleteChatMessage(chatMessageId), {
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(['chats', 'messages', chat.id]);
|
||||
queryClient.invalidateQueries(chatKeys.chatMessages(chat.id));
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -241,6 +243,18 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
|
|||
icon: require('@tabler/icons/trash.svg'),
|
||||
destructive: true,
|
||||
});
|
||||
} else {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.report),
|
||||
action: () => null, // TODO: implement once API is available
|
||||
icon: require('@tabler/icons/flag.svg'),
|
||||
});
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.deleteForMe),
|
||||
action: () => null, // TODO: implement once API is available
|
||||
icon: require('@tabler/icons/trash.svg'),
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
|
@ -19,7 +19,7 @@ const messages = defineMessages({
|
|||
blockMessage: { id: 'chat_settings.block.message', defaultMessage: 'Blocking will prevent this profile from direct messaging you and viewing your content. You can unblock later.' },
|
||||
blockHeading: { id: 'chat_settings.block.heading', defaultMessage: 'Block @{acct}' },
|
||||
blockConfirm: { id: 'chat_settings.block.confirm', defaultMessage: 'Block' },
|
||||
leaveMessage: { id: 'chat_settings.leave.message', defaultMessage: 'Are you sure you want to leave this chat? This conversation will be removed from your inbox.' },
|
||||
leaveMessage: { id: 'chat_settings.leave.message', defaultMessage: 'Are you sure you want to leave this chat? Messages will be deleted for you and this chat will be removed from your inbox.' },
|
||||
leaveHeading: { id: 'chat_settings.leave.heading', defaultMessage: 'Leave Chat' },
|
||||
leaveConfirm: { id: 'chat_settings.leave.confirm', defaultMessage: 'Leave Chat' },
|
||||
blockUser: { id: 'chat_settings.options.block_user', defaultMessage: 'Block @{acct}' },
|
||||
|
@ -38,7 +38,7 @@ const ChatPageMain: React.FC<IChatPageMain> = ({ chatId }) => {
|
|||
const account = useOwnAccount();
|
||||
|
||||
const { chat, deleteChat } = useChat(chatId);
|
||||
const { isSilenced, handleSilence } = useChatSilence(chat?.data);
|
||||
const { isSilenced, handleSilence, fetchChatSilence } = useChatSilence(chat?.data);
|
||||
|
||||
const handleBlockUser = () => {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
|
@ -62,6 +62,12 @@ const ChatPageMain: React.FC<IChatPageMain> = ({ chatId }) => {
|
|||
|
||||
const handleReportChat = () => dispatch(initReport(chat?.data?.account as any));
|
||||
|
||||
useEffect(() => {
|
||||
if (chatId) {
|
||||
fetchChatSilence();
|
||||
}
|
||||
}, [chatId]);
|
||||
|
||||
if (!chat && !account?.chats_onboarded) {
|
||||
return (
|
||||
<Welcome />
|
||||
|
|
|
@ -19,7 +19,7 @@ const Blankslate = ({ onSearch }: IBlankslate) => {
|
|||
return (
|
||||
<Stack alignItems='center' justifyContent='center' className='h-full flex-grow'>
|
||||
<Stack space={4}>
|
||||
<Stack space={1} className='max-w-[85%] mx-auto'>
|
||||
<Stack space={1} className='max-w-[80%] mx-auto'>
|
||||
<Text size='lg' weight='bold' align='center'>
|
||||
{intl.formatMessage(messages.title)}
|
||||
</Text>
|
||||
|
|
|
@ -3,6 +3,7 @@ import React, { useState } from 'react';
|
|||
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import { 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';
|
||||
|
||||
|
@ -19,14 +20,13 @@ import Blankslate from './blankslate';
|
|||
const ChatPane = () => {
|
||||
const features = useFeatures();
|
||||
const debounce = useDebounce;
|
||||
const { unreadChatsCount } = useStatContext();
|
||||
|
||||
const [value, setValue] = useState<string>();
|
||||
const debouncedValue = debounce(value as string, 300);
|
||||
|
||||
const { chat, setChat, isOpen, isSearching, setSearching, toggleChatPane } = useChatContext();
|
||||
const { chatsQuery: { data: chats } } = useChats(debouncedValue);
|
||||
|
||||
const unreadCount = sumBy(chats, (chat) => chat.unread);
|
||||
const { chatsQuery: { data: chats, isLoading } } = useChats(debouncedValue);
|
||||
|
||||
const hasSearchValue = Number(debouncedValue?.length) > 0;
|
||||
|
||||
|
@ -42,7 +42,7 @@ const ChatPane = () => {
|
|||
};
|
||||
|
||||
const renderBody = () => {
|
||||
if (hasSearchValue || Number(chats?.length) > 0) {
|
||||
if (hasSearchValue || Number(chats?.length) > 0 || isLoading) {
|
||||
return (
|
||||
<Stack space={4} className='flex-grow h-full'>
|
||||
{features.chatsSearch && (
|
||||
|
@ -55,11 +55,10 @@ const ChatPane = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{Number(chats?.length) > 0 ? (
|
||||
{(Number(chats?.length) > 0 || isLoading) ? (
|
||||
<ChatList
|
||||
searchValue={debouncedValue}
|
||||
onClickChat={handleClickChat}
|
||||
fade
|
||||
/>
|
||||
) : (
|
||||
<EmptyResultsBlankslate />
|
||||
|
@ -90,12 +89,16 @@ const ChatPane = () => {
|
|||
<Pane isOpen={isOpen} index={0} main>
|
||||
<ChatPaneHeader
|
||||
title='Messages'
|
||||
unreadCount={unreadCount}
|
||||
unreadCount={unreadChatsCount}
|
||||
isOpen={isOpen}
|
||||
onToggle={toggleChatPane}
|
||||
secondaryAction={() => {
|
||||
setSearching(true);
|
||||
setValue(undefined);
|
||||
|
||||
if (!isOpen) {
|
||||
toggleChatPane();
|
||||
}
|
||||
}}
|
||||
secondaryActionIcon={require('@tabler/icons/edit.svg')}
|
||||
/>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useChats } from 'soapbox/queries/chats';
|
|||
import { queryClient } from 'soapbox/queries/client';
|
||||
import useAccountSearch from 'soapbox/queries/search';
|
||||
|
||||
import { chatKeys } from '../../../../queries/chats';
|
||||
import ChatPaneHeader from '../chat-pane-header';
|
||||
import { Pane } from '../ui';
|
||||
|
||||
|
@ -47,7 +48,7 @@ const ChatSearch = () => {
|
|||
},
|
||||
onSuccess: (response) => {
|
||||
setChat(response.data);
|
||||
queryClient.invalidateQueries(['chats', 'search']);
|
||||
queryClient.invalidateQueries(chatKeys.chatSearch());
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { blockAccount } from 'soapbox/actions/accounts';
|
||||
|
@ -16,7 +16,7 @@ const messages = defineMessages({
|
|||
blockMessage: { id: 'chat_settings.block.message', defaultMessage: 'Blocking will prevent this profile from direct messaging you and viewing your content. You can unblock later.' },
|
||||
blockHeading: { id: 'chat_settings.block.heading', defaultMessage: 'Block @{acct}' },
|
||||
blockConfirm: { id: 'chat_settings.block.confirm', defaultMessage: 'Block' },
|
||||
leaveMessage: { id: 'chat_settings.leave.message', defaultMessage: 'Are you sure you want to leave this chat? This conversation will be removed from your inbox.' },
|
||||
leaveMessage: { id: 'chat_settings.leave.message', defaultMessage: 'Are you sure you want to leave this chat? Messages will be deleted for you and this chat will be removed from your inbox.' },
|
||||
leaveHeading: { id: 'chat_settings.leave.heading', defaultMessage: 'Leave Chat' },
|
||||
leaveConfirm: { id: 'chat_settings.leave.confirm', defaultMessage: 'Leave Chat' },
|
||||
title: { id: 'chat_settings.title', defaultMessage: 'Chat Details' },
|
||||
|
@ -30,7 +30,7 @@ const ChatSettings = () => {
|
|||
const intl = useIntl();
|
||||
|
||||
const { chat, setEditing, toggleChatPane } = useChatContext();
|
||||
const { isSilenced, handleSilence } = useChatSilence(chat);
|
||||
const { isSilenced, handleSilence, fetchChatSilence } = useChatSilence(chat);
|
||||
|
||||
const { deleteChat } = useChat(chat?.id as string);
|
||||
|
||||
|
@ -63,6 +63,12 @@ const ChatSettings = () => {
|
|||
|
||||
const handleReportChat = () => dispatch(initReport(chat?.account as any));
|
||||
|
||||
useEffect(() => {
|
||||
if (chat?.id) {
|
||||
fetchChatSilence();
|
||||
}
|
||||
}, [chat?.id]);
|
||||
|
||||
if (!chat) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { HStack, IconButton, Stack, Text, Textarea } from 'soapbox/components/ui
|
|||
import UploadProgress from 'soapbox/components/upload-progress';
|
||||
import UploadButton from 'soapbox/features/compose/components/upload_button';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import { IChat, useChat } from 'soapbox/queries/chats';
|
||||
import { chatKeys, IChat, useChat } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { truncateFilename } from 'soapbox/utils/media';
|
||||
|
||||
|
@ -97,7 +97,7 @@ const Chat: React.FC<ChatInterface> = ({ chat, autosize, inputRef, className })
|
|||
},
|
||||
// Always refetch after error or success:
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['chats', 'messages', chat.id]);
|
||||
queryClient.invalidateQueries(chatKeys.chatMessages(chat.id));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -36,7 +36,9 @@ const ReplyMentions: React.FC<IReplyMentions> = ({ composeId }) => {
|
|||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch(openModal('REPLY_MENTIONS'));
|
||||
dispatch(openModal('REPLY_MENTIONS', {
|
||||
composeId,
|
||||
}));
|
||||
};
|
||||
|
||||
if (!parentTo || (parentTo.size === 0)) {
|
||||
|
|
|
@ -11,7 +11,7 @@ import ActionButton from '../ui/components/action-button';
|
|||
import type { Account } from 'soapbox/types/entities';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'feed_suggestions.heading', defaultMessage: 'Suggested profiles' },
|
||||
heading: { id: 'feed_suggestions.heading', defaultMessage: 'Suggested Profiles' },
|
||||
viewAll: { id: 'feed_suggestions.view_all', defaultMessage: 'View all' },
|
||||
});
|
||||
|
||||
|
@ -65,7 +65,7 @@ const FeedSuggestions = () => {
|
|||
if (!isLoading && suggestedProfiles.size === 0) return null;
|
||||
|
||||
return (
|
||||
<Card size='lg' variant='rounded' className='space-y-4'>
|
||||
<Card size='lg' variant='rounded' className='space-y-6'>
|
||||
<HStack justifyContent='between' alignItems='center'>
|
||||
<CardTitle title={intl.formatMessage(messages.heading)} />
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import Column from 'soapbox/features/ui/components/column';
|
|||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'followRecommendations.heading', defaultMessage: 'Suggested profiles' },
|
||||
heading: { id: 'followRecommendations.heading', defaultMessage: 'Suggested Profiles' },
|
||||
});
|
||||
|
||||
const FollowRecommendations: React.FC = () => {
|
||||
|
|
|
@ -17,10 +17,6 @@ const BioStep = ({ onNext }: { onNext: () => void }) => {
|
|||
const [isSubmitting, setSubmitting] = React.useState<boolean>(false);
|
||||
const [errors, setErrors] = React.useState<string[]>([]);
|
||||
|
||||
const trimmedValue = value.trim();
|
||||
const isValid = trimmedValue.length > 0;
|
||||
const isDisabled = !isValid;
|
||||
|
||||
const handleSubmit = () => {
|
||||
setSubmitting(true);
|
||||
|
||||
|
@ -79,7 +75,7 @@ const BioStep = ({ onNext }: { onNext: () => void }) => {
|
|||
block
|
||||
theme='primary'
|
||||
type='submit'
|
||||
disabled={isDisabled || isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
|
|
|
@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||
import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
import useOnboardingSuggestions from 'soapbox/queries/suggestions';
|
||||
import { useOnboardingSuggestions } from 'soapbox/queries/suggestions';
|
||||
|
||||
const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const { data, fetchNextPage, hasNextPage, isFetching } = useOnboardingSuggestions();
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
|
||||
import { HStack, Stack } from 'soapbox/components/ui';
|
||||
|
||||
import { randomIntFromInterval, generateText } from '../utils';
|
||||
|
||||
export default ({ limit }: { limit: number }) => {
|
||||
const length = randomIntFromInterval(15, 3);
|
||||
const acctLength = randomIntFromInterval(15, 3);
|
||||
|
||||
return (
|
||||
<>
|
||||
{new Array(limit).fill(undefined).map((_, idx) => (
|
||||
<HStack key={idx} alignItems='center' space={2} className='animate-pulse'>
|
||||
<Stack space={3} className='text-center'>
|
||||
<div
|
||||
className='w-9 h-9 block mx-auto rounded-full bg-primary-200 dark:bg-primary-700'
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack className='text-primary-200 dark:text-primary-700'>
|
||||
<p>{generateText(length)}</p>
|
||||
<p>{generateText(acctLength)}</p>
|
||||
</Stack>
|
||||
</HStack>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -19,7 +19,7 @@ const Share = () => {
|
|||
.join('\n\n');
|
||||
|
||||
if (text) {
|
||||
dispatch(openComposeWithText(text));
|
||||
dispatch(openComposeWithText('compose-modal', text));
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -27,4 +27,4 @@ const Share = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Share;
|
||||
export default Share;
|
||||
|
|
|
@ -1,123 +1,201 @@
|
|||
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../../../jest/test-helpers';
|
||||
import { normalizeAccount } from '../../../../normalizers';
|
||||
import { __stub } from 'soapbox/api';
|
||||
|
||||
import { render, rootState, screen, waitFor } from '../../../../jest/test-helpers';
|
||||
import { normalizeInstance } from '../../../../normalizers';
|
||||
import WhoToFollowPanel from '../who-to-follow-panel';
|
||||
|
||||
const buildTruthSuggestion = (id: string) => ({
|
||||
account_avatar: 'avatar',
|
||||
account_id: id,
|
||||
acct: 'acct',
|
||||
display_name: 'my name',
|
||||
note: 'hello',
|
||||
verified: true,
|
||||
});
|
||||
|
||||
const buildSuggestion = (id: string) => ({
|
||||
source: 'staff',
|
||||
account: {
|
||||
username: 'username',
|
||||
verified: true,
|
||||
id,
|
||||
acct: 'acct',
|
||||
avatar: 'avatar',
|
||||
avatar_static: 'avatar',
|
||||
display_name: 'my name',
|
||||
},
|
||||
});
|
||||
|
||||
describe('<WhoToFollow />', () => {
|
||||
it('renders suggested accounts', () => {
|
||||
const store = {
|
||||
accounts: ImmutableMap({
|
||||
'1': normalizeAccount({
|
||||
id: '1',
|
||||
acct: 'username',
|
||||
display_name: 'My name',
|
||||
avatar: 'test.jpg',
|
||||
}),
|
||||
}),
|
||||
suggestions: {
|
||||
items: ImmutableOrderedSet([{
|
||||
source: 'staff',
|
||||
account: '1',
|
||||
}]),
|
||||
},
|
||||
};
|
||||
let store: any;
|
||||
|
||||
render(<WhoToFollowPanel limit={1} />, undefined, store);
|
||||
expect(screen.getByTestId('account')).toHaveTextContent(/my name/i);
|
||||
describe('using Truth Social software', () => {
|
||||
beforeEach(() => {
|
||||
store = rootState
|
||||
.set('me', '1234')
|
||||
.set('instance', normalizeInstance({
|
||||
version: '3.4.1 (compatible; TruthSocial 1.0.0)',
|
||||
}));
|
||||
});
|
||||
|
||||
describe('with a single suggestion', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/truth/carousels/suggestions')
|
||||
.reply(200, [buildTruthSuggestion('1')], {
|
||||
link: '<https://example.com/api/v1/truth/carousels/suggestions?since_id=1>; rel=\'prev\'',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders suggested accounts', async () => {
|
||||
render(<WhoToFollowPanel limit={1} />, undefined, store);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('account')).toHaveTextContent(/my name/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a multiple suggestion', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/truth/carousels/suggestions')
|
||||
.reply(200, [buildTruthSuggestion('1'), buildTruthSuggestion('2')], {
|
||||
link: '<https://example.com/api/v1/truth/carousels/suggestions?since_id=1>; rel=\'prev\'',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders suggested accounts', async () => {
|
||||
render(<WhoToFollowPanel limit={2} />, undefined, store);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('account')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a set limit', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/truth/carousels/suggestions')
|
||||
.reply(200, [buildTruthSuggestion('1'), buildTruthSuggestion('2')], {
|
||||
link: '<https://example.com/api/v1/truth/carousels/suggestions?since_id=1>; rel=\'prev\'',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('respects the limit prop', async () => {
|
||||
render(<WhoToFollowPanel limit={1} />, undefined, store);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('account')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the API returns an empty list', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/truth/carousels/suggestions')
|
||||
.reply(200, [], {
|
||||
link: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders empty', async () => {
|
||||
render(<WhoToFollowPanel limit={1} />, undefined, store);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('account')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders multiple accounts', () => {
|
||||
const store = {
|
||||
accounts: ImmutableMap({
|
||||
'1': normalizeAccount({
|
||||
id: '1',
|
||||
acct: 'username',
|
||||
display_name: 'My name',
|
||||
avatar: 'test.jpg',
|
||||
}),
|
||||
'2': normalizeAccount({
|
||||
id: '1',
|
||||
acct: 'username2',
|
||||
display_name: 'My other name',
|
||||
avatar: 'test.jpg',
|
||||
}),
|
||||
}),
|
||||
suggestions: {
|
||||
items: ImmutableOrderedSet([
|
||||
{
|
||||
source: 'staff',
|
||||
account: '1',
|
||||
},
|
||||
{
|
||||
source: 'staff',
|
||||
account: '2',
|
||||
},
|
||||
]),
|
||||
},
|
||||
};
|
||||
describe('using Pleroma software', () => {
|
||||
beforeEach(() => {
|
||||
store = rootState.set('me', '1234');
|
||||
});
|
||||
|
||||
render(<WhoToFollowPanel limit={3} />, undefined, store);
|
||||
expect(screen.queryAllByTestId('account')).toHaveLength(2);
|
||||
});
|
||||
describe('with a single suggestion', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v2/suggestions')
|
||||
.reply(200, [buildSuggestion('1')], {
|
||||
link: '<https://example.com/api/v2/suggestions?since_id=1>; rel=\'prev\'',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('respects the limit prop', () => {
|
||||
const store = {
|
||||
accounts: ImmutableMap({
|
||||
'1': normalizeAccount({
|
||||
id: '1',
|
||||
acct: 'username',
|
||||
display_name: 'My name',
|
||||
avatar: 'test.jpg',
|
||||
}),
|
||||
'2': normalizeAccount({
|
||||
id: '1',
|
||||
acct: 'username2',
|
||||
display_name: 'My other name',
|
||||
avatar: 'test.jpg',
|
||||
}),
|
||||
}),
|
||||
suggestions: {
|
||||
items: ImmutableOrderedSet([
|
||||
{
|
||||
source: 'staff',
|
||||
account: '1',
|
||||
},
|
||||
{
|
||||
source: 'staff',
|
||||
account: '2',
|
||||
},
|
||||
]),
|
||||
},
|
||||
};
|
||||
it('renders suggested accounts', async () => {
|
||||
render(<WhoToFollowPanel limit={1} />, undefined, store);
|
||||
|
||||
render(<WhoToFollowPanel limit={1} />, undefined, store);
|
||||
expect(screen.queryAllByTestId('account')).toHaveLength(1);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('account')).toHaveTextContent(/my name/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders empty', () => {
|
||||
const store = {
|
||||
accounts: ImmutableMap({
|
||||
'1': normalizeAccount({
|
||||
id: '1',
|
||||
acct: 'username',
|
||||
display_name: 'My name',
|
||||
avatar: 'test.jpg',
|
||||
}),
|
||||
'2': normalizeAccount({
|
||||
id: '1',
|
||||
acct: 'username2',
|
||||
display_name: 'My other name',
|
||||
avatar: 'test.jpg',
|
||||
}),
|
||||
}),
|
||||
suggestions: {
|
||||
items: ImmutableOrderedSet([]),
|
||||
},
|
||||
};
|
||||
describe('with a multiple suggestion', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v2/suggestions')
|
||||
.reply(200, [buildSuggestion('1'), buildSuggestion('2')], {
|
||||
link: '<https://example.com/api/v2/suggestions?since_id=1>; rel=\'prev\'',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
render(<WhoToFollowPanel limit={1} />, undefined, store);
|
||||
expect(screen.queryAllByTestId('account')).toHaveLength(0);
|
||||
it('renders suggested accounts', async () => {
|
||||
render(<WhoToFollowPanel limit={2} />, undefined, store);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('account')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a set limit', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v2/suggestions')
|
||||
.reply(200, [buildSuggestion('1'), buildSuggestion('2')], {
|
||||
link: '<https://example.com/api/v2/suggestions?since_id=1>; rel=\'prev\'',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('respects the limit prop', async () => {
|
||||
render(<WhoToFollowPanel limit={1} />, undefined, store);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('account')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the API returns an empty list', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v2/suggestions')
|
||||
.reply(200, [], {
|
||||
link: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders empty', async () => {
|
||||
render(<WhoToFollowPanel limit={1} />, undefined, store);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('account')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Modal } from 'soapbox/components/ui';
|
||||
|
@ -18,7 +18,8 @@ interface IReplyMentionsModal {
|
|||
const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ composeId, onClose }) => {
|
||||
const compose = useCompose(composeId);
|
||||
|
||||
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: compose.in_reply_to! }));
|
||||
const getStatus = useCallback(makeGetStatus(), []);
|
||||
const status = useAppSelector<StatusEntity | null>(state => getStatus(state, { id: compose.in_reply_to! }));
|
||||
const account = useAppSelector((state) => state.accounts.get(state.me));
|
||||
|
||||
const mentions = statusToMentionsAccountIdsArray(status!, account!);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import * as React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { fetchSuggestions, dismissSuggestion } from 'soapbox/actions/suggestions';
|
||||
import { Widget } from 'soapbox/components/ui';
|
||||
import { Text, Widget } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import PlaceholderSidebarSuggestions from 'soapbox/features/placeholder/components/placeholder-sidebar-suggestions';
|
||||
import { useDismissSuggestion, useSuggestions } from 'soapbox/queries/suggestions';
|
||||
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -18,44 +18,40 @@ interface IWhoToFollowPanel {
|
|||
}
|
||||
|
||||
const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const suggestions = useAppSelector((state) => state.suggestions.items);
|
||||
const { data: suggestions, isFetching } = useSuggestions();
|
||||
const dismissSuggestion = useDismissSuggestion();
|
||||
|
||||
const suggestionsToRender = suggestions.slice(0, limit);
|
||||
|
||||
const handleDismiss = (account: AccountEntity) => {
|
||||
dispatch(dismissSuggestion(account.id));
|
||||
dismissSuggestion.mutate(account.id);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(fetchSuggestions());
|
||||
}, []);
|
||||
|
||||
if (suggestionsToRender.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// FIXME: This page actually doesn't look good right now
|
||||
// const handleAction = () => {
|
||||
// history.push('/suggestions');
|
||||
// };
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={<FormattedMessage id='who_to_follow.title' defaultMessage='People To Follow' />}
|
||||
// onAction={handleAction}
|
||||
action={
|
||||
<Link to='/suggestions'>
|
||||
<Text tag='span' theme='primary' size='sm' className='hover:underline'>View all</Text>
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
{suggestionsToRender.map((suggestion) => (
|
||||
<AccountContainer
|
||||
key={suggestion.account}
|
||||
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
|
||||
id={suggestion.account}
|
||||
actionIcon={require('@tabler/icons/x.svg')}
|
||||
actionTitle={intl.formatMessage(messages.dismissSuggestion)}
|
||||
onActionClick={handleDismiss}
|
||||
/>
|
||||
))}
|
||||
{isFetching ? (
|
||||
<PlaceholderSidebarSuggestions limit={limit} />
|
||||
) : (
|
||||
suggestionsToRender.map((suggestion: any) => (
|
||||
<AccountContainer
|
||||
key={suggestion.account}
|
||||
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
|
||||
id={suggestion.account}
|
||||
actionIcon={require('@tabler/icons/x.svg')}
|
||||
actionTitle={intl.formatMessage(messages.dismissSuggestion)}
|
||||
onActionClick={handleDismiss}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -117,6 +117,7 @@ import { WrappedRoute } from './util/react_router_helpers';
|
|||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||
// Without this it ends up in ~8 very commonly used bundles.
|
||||
import 'soapbox/components/status';
|
||||
import { StatProvider } from '../../contexts/stat-context';
|
||||
|
||||
const EmptyPage = HomePage;
|
||||
|
||||
|
@ -650,52 +651,54 @@ const UI: React.FC = ({ children }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<HotKeys keyMap={keyMap} handlers={me ? handlers : undefined} ref={setHotkeysRef} attach={window} focused>
|
||||
<div ref={node} style={style}>
|
||||
<BackgroundShapes />
|
||||
<StatProvider>
|
||||
<HotKeys keyMap={keyMap} handlers={me ? handlers : undefined} ref={setHotkeysRef} attach={window} focused>
|
||||
<div ref={node} style={style}>
|
||||
<BackgroundShapes />
|
||||
|
||||
<div className='z-10 flex flex-col'>
|
||||
<Navbar />
|
||||
<div className='z-10 flex flex-col'>
|
||||
<Navbar />
|
||||
|
||||
<Layout>
|
||||
<Layout.Sidebar>
|
||||
{!standalone && <SidebarNavigation />}
|
||||
</Layout.Sidebar>
|
||||
<Layout>
|
||||
<Layout.Sidebar>
|
||||
{!standalone && <SidebarNavigation />}
|
||||
</Layout.Sidebar>
|
||||
|
||||
<SwitchingColumnsArea>
|
||||
{children}
|
||||
</SwitchingColumnsArea>
|
||||
</Layout>
|
||||
<SwitchingColumnsArea>
|
||||
{children}
|
||||
</SwitchingColumnsArea>
|
||||
</Layout>
|
||||
|
||||
{me && floatingActionButton}
|
||||
{me && floatingActionButton}
|
||||
|
||||
<BundleContainer fetchComponent={UploadArea}>
|
||||
{Component => <Component active={draggingOver} onClose={closeUploadModal} />}
|
||||
</BundleContainer>
|
||||
<BundleContainer fetchComponent={UploadArea}>
|
||||
{Component => <Component active={draggingOver} onClose={closeUploadModal} />}
|
||||
</BundleContainer>
|
||||
|
||||
{me && (
|
||||
<BundleContainer fetchComponent={SidebarMenu}>
|
||||
{me && (
|
||||
<BundleContainer fetchComponent={SidebarMenu}>
|
||||
{Component => <Component />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
|
||||
{me && features.chats && !mobile && (
|
||||
<BundleContainer fetchComponent={ChatWidget}>
|
||||
{Component => <Component />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
<ThumbNavigation />
|
||||
|
||||
<BundleContainer fetchComponent={ProfileHoverCard}>
|
||||
{Component => <Component />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
|
||||
{me && features.chats && !mobile && (
|
||||
<BundleContainer fetchComponent={ChatWidget}>
|
||||
<BundleContainer fetchComponent={StatusHoverCard}>
|
||||
{Component => <Component />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
<ThumbNavigation />
|
||||
|
||||
<BundleContainer fetchComponent={ProfileHoverCard}>
|
||||
{Component => <Component />}
|
||||
</BundleContainer>
|
||||
|
||||
<BundleContainer fetchComponent={StatusHoverCard}>
|
||||
{Component => <Component />}
|
||||
</BundleContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</HotKeys>
|
||||
</StatProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ const DefaultPage: React.FC = ({ children }) => {
|
|||
)}
|
||||
{features.suggestions && (
|
||||
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
||||
{Component => <Component limit={5} key='wtf-panel' />}
|
||||
{Component => <Component limit={3} key='wtf-panel' />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
<LinkFooter key='link-footer' />
|
||||
|
|
|
@ -105,7 +105,7 @@ const HomePage: React.FC = ({ children }) => {
|
|||
)}
|
||||
{features.suggestions && (
|
||||
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
||||
{Component => <Component limit={5} />}
|
||||
{Component => <Component limit={3} />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
<LinkFooter key='link-footer' />
|
||||
|
|
|
@ -139,7 +139,7 @@ const ProfilePage: React.FC<IProfilePage> = ({ params, children }) => {
|
|||
</BundleContainer>
|
||||
) : features.suggestions && (
|
||||
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
||||
{Component => <Component limit={5} key='wtf-panel' />}
|
||||
{Component => <Component limit={3} key='wtf-panel' />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
<LinkFooter key='link-footer' />
|
||||
|
|
|
@ -45,7 +45,7 @@ const StatusPage: React.FC<IStatusPage> = ({ children }) => {
|
|||
)}
|
||||
{features.suggestions && (
|
||||
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
||||
{Component => <Component limit={5} key='wtf-panel' />}
|
||||
{Component => <Component limit={3} key='wtf-panel' />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
<LinkFooter key='link-footer' />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { __stub } from 'soapbox/api';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import useOnboardingSuggestions from '../suggestions';
|
||||
import { useOnboardingSuggestions } from '../suggestions';
|
||||
|
||||
describe('useCarouselAvatars', () => {
|
||||
describe('with a successful query', () => {
|
||||
|
@ -17,7 +17,7 @@ describe('useCarouselAvatars', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('is successful', async() => {
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useOnboardingSuggestions());
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
@ -33,7 +33,7 @@ describe('useCarouselAvatars', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('is successful', async() => {
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useOnboardingSuggestions());
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
|
|
@ -4,7 +4,11 @@ import { Ad, getProvider } from 'soapbox/features/ads/providers';
|
|||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { isExpired } from 'soapbox/utils/ads';
|
||||
|
||||
export default function useAds() {
|
||||
const adKeys = {
|
||||
ads: ['ads'] as const,
|
||||
};
|
||||
|
||||
function useAds() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getAds = async() => {
|
||||
|
@ -18,7 +22,7 @@ export default function useAds() {
|
|||
});
|
||||
};
|
||||
|
||||
const result = useQuery<Ad[]>(['ads'], getAds, {
|
||||
const result = useQuery<Ad[]>(adKeys.ads, getAds, {
|
||||
placeholderData: [],
|
||||
});
|
||||
|
||||
|
@ -30,3 +34,5 @@ export default function useAds() {
|
|||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export { useAds as default, adKeys };
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import sumBy from 'lodash/sumBy';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||
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 { useStatContext } from 'soapbox/contexts/stat-context';
|
||||
import { useApi, useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||
import { normalizeChatMessage } from 'soapbox/normalizers';
|
||||
import { flattenPages, updatePageItem } from 'soapbox/utils/queries';
|
||||
import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries';
|
||||
|
||||
import { queryClient } from './client';
|
||||
|
||||
|
@ -50,18 +52,18 @@ export interface IChatSilence {
|
|||
target_account_id: number
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
result: T[],
|
||||
hasMore: boolean,
|
||||
link?: string,
|
||||
}
|
||||
const chatKeys = {
|
||||
chatMessages: (chatId: string) => ['chats', 'messages', chatId] as const,
|
||||
chatSearch: (searchQuery?: string) => ['chats', 'search', searchQuery] as const,
|
||||
chatSilences: ['chatSilences'] as const,
|
||||
};
|
||||
|
||||
const reverseOrder = (a: IChat, b: IChat): number => compareId(a.id, b.id);
|
||||
|
||||
const useChatMessages = (chatId: string) => {
|
||||
const api = useApi();
|
||||
|
||||
const getChatMessages = async(chatId: string, pageParam?: any): Promise<PaginatedResult<IChatMessage>> => {
|
||||
const getChatMessages = async (chatId: string, pageParam?: any): Promise<PaginatedResult<IChatMessage>> => {
|
||||
const nextPageLink = pageParam?.link;
|
||||
const uri = nextPageLink || `/api/v1/pleroma/chats/${chatId}/messages`;
|
||||
const response = await api.get(uri);
|
||||
|
@ -78,7 +80,7 @@ const useChatMessages = (chatId: string) => {
|
|||
};
|
||||
};
|
||||
|
||||
const queryInfo = useInfiniteQuery(['chats', 'messages', chatId], ({ pageParam }) => getChatMessages(chatId, pageParam), {
|
||||
const queryInfo = useInfiniteQuery(chatKeys.chatMessages(chatId), ({ pageParam }) => getChatMessages(chatId, pageParam), {
|
||||
keepPreviousData: true,
|
||||
getNextPageParam: (config) => {
|
||||
if (config.hasMore) {
|
||||
|
@ -101,8 +103,9 @@ const useChats = (search?: string) => {
|
|||
const api = useApi();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const { setUnreadChatsCount } = useStatContext();
|
||||
|
||||
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 uri = nextPageLink || endpoint;
|
||||
|
@ -116,6 +119,9 @@ const useChats = (search?: string) => {
|
|||
const link = getNextLink(response);
|
||||
const hasMore = !!link;
|
||||
|
||||
// TODO: change to response header
|
||||
setUnreadChatsCount(sumBy(data, (chat) => chat.unread));
|
||||
|
||||
// Set the relationships to these users in the redux store.
|
||||
dispatch(fetchRelationships(data.map((item) => item.account.id)));
|
||||
|
||||
|
@ -126,7 +132,7 @@ const useChats = (search?: string) => {
|
|||
};
|
||||
};
|
||||
|
||||
const queryInfo = useInfiniteQuery(['chats', 'search', search], ({ pageParam }) => getChats(pageParam), {
|
||||
const queryInfo = useInfiniteQuery(chatKeys.chatSearch(search), ({ pageParam }) => getChats(pageParam), {
|
||||
keepPreviousData: true,
|
||||
getNextPageParam: (config) => {
|
||||
if (config.hasMore) {
|
||||
|
@ -181,8 +187,8 @@ const useChat = (chatId?: string) => {
|
|||
const acceptChat = useMutation(() => api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/accept`), {
|
||||
onSuccess(response) {
|
||||
setChat(response.data);
|
||||
queryClient.invalidateQueries(['chats', 'messages', chatId]);
|
||||
queryClient.invalidateQueries(['chats', 'search']);
|
||||
queryClient.invalidateQueries(chatKeys.chatMessages(chatId));
|
||||
queryClient.invalidateQueries(chatKeys.chatSearch());
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -190,8 +196,8 @@ const useChat = (chatId?: string) => {
|
|||
onSuccess(response) {
|
||||
setChat(null);
|
||||
setEditing(false);
|
||||
queryClient.invalidateQueries(['chats', 'messages', chatId]);
|
||||
queryClient.invalidateQueries(['chats', 'search']);
|
||||
queryClient.invalidateQueries(chatKeys.chatMessages(chatId));
|
||||
queryClient.invalidateQueries(chatKeys.chatSearch());
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -201,13 +207,13 @@ const useChat = (chatId?: string) => {
|
|||
const useChatSilences = () => {
|
||||
const api = useApi();
|
||||
|
||||
const getChatSilences = async() => {
|
||||
const getChatSilences = async () => {
|
||||
const { data } = await api.get<IChatSilence[]>('/api/v1/pleroma/chats/silences');
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return useQuery<IChatSilence[]>(['chatSilences'], getChatSilences, {
|
||||
return useQuery<IChatSilence[]>(chatKeys.chatSilences, getChatSilences, {
|
||||
placeholderData: [],
|
||||
});
|
||||
};
|
||||
|
@ -218,12 +224,12 @@ const useChatSilence = (chat?: IChat) => {
|
|||
|
||||
const [isSilenced, setSilenced] = useState<boolean>(false);
|
||||
|
||||
const getChatSilences = async() => {
|
||||
const getChatSilences = async () => {
|
||||
const { data } = await api.get(`api/v1/pleroma/chats/silence?account_id=${chat?.account.id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
const fetchChatSilence = async() => {
|
||||
const fetchChatSilence = async () => {
|
||||
const data = await getChatSilences();
|
||||
if (data) {
|
||||
setSilenced(true);
|
||||
|
@ -246,6 +252,7 @@ const useChatSilence = (chat?: IChat) => {
|
|||
api.post(`api/v1/pleroma/chats/silence?account_id=${chat?.account.id}`)
|
||||
.then(() => {
|
||||
dispatch(snackbar.success('Successfully silenced this chat.'));
|
||||
queryClient.invalidateQueries(chatKeys.chatSilences);
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(snackbar.error('Something went wrong trying to silence this chat. Please try again.'));
|
||||
|
@ -259,6 +266,7 @@ const useChatSilence = (chat?: IChat) => {
|
|||
api.delete(`api/v1/pleroma/chats/silence?account_id=${chat?.account.id}`)
|
||||
.then(() => {
|
||||
dispatch(snackbar.success('Successfully unsilenced this chat.'));
|
||||
queryClient.invalidateQueries(chatKeys.chatSilences);
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(snackbar.error('Something went wrong trying to unsilence this chat. Please try again.'));
|
||||
|
@ -266,13 +274,7 @@ const useChatSilence = (chat?: IChat) => {
|
|||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (chat?.id) {
|
||||
fetchChatSilence();
|
||||
}
|
||||
}, [chat?.id]);
|
||||
|
||||
return { isSilenced, handleSilence };
|
||||
return { isSilenced, handleSilence, fetchChatSilence };
|
||||
};
|
||||
|
||||
export { useChat, useChats, useChatMessages, useChatSilences, useChatSilence };
|
||||
export { chatKeys, useChat, useChats, useChatMessages, useChatSilences, useChatSilence };
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useInfiniteQuery, useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||
import { importFetchedAccounts } from 'soapbox/actions/importer';
|
||||
import { SuggestedProfile } from 'soapbox/actions/suggestions';
|
||||
import { getLinks } from 'soapbox/api';
|
||||
import { useApi, useAppDispatch } from 'soapbox/hooks';
|
||||
import { useApi, useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
import { PaginatedResult, removePageItem } from '../utils/queries';
|
||||
|
||||
import type { IAccount } from './accounts';
|
||||
|
||||
|
@ -12,11 +15,124 @@ type Suggestion = {
|
|||
account: IAccount
|
||||
}
|
||||
|
||||
export default function useOnboardingSuggestions() {
|
||||
type TruthSuggestion = {
|
||||
account_avatar: string
|
||||
account_id: string
|
||||
acct: string
|
||||
display_name: string
|
||||
note: string
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
type Result = TruthSuggestion | {
|
||||
account: string
|
||||
}
|
||||
|
||||
type PageParam = {
|
||||
link?: string
|
||||
}
|
||||
|
||||
const suggestionKeys = {
|
||||
suggestions: ['suggestions'] as const,
|
||||
};
|
||||
|
||||
const mapSuggestedProfileToAccount = (suggestedProfile: SuggestedProfile) => ({
|
||||
id: suggestedProfile.account_id,
|
||||
avatar: suggestedProfile.account_avatar,
|
||||
avatar_static: suggestedProfile.account_avatar,
|
||||
acct: suggestedProfile.acct,
|
||||
display_name: suggestedProfile.display_name,
|
||||
note: suggestedProfile.note,
|
||||
verified: suggestedProfile.verified,
|
||||
});
|
||||
|
||||
const useSuggestions = () => {
|
||||
const api = useApi();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
|
||||
const getV2Suggestions = async (pageParam: PageParam): Promise<PaginatedResult<Result>> => {
|
||||
const endpoint = pageParam?.link || '/api/v2/suggestions';
|
||||
const response = await api.get<Suggestion[]>(endpoint);
|
||||
const hasMore = !!response.headers.link;
|
||||
const nextLink = getLinks(response).refs.find(link => link.rel === 'next')?.uri;
|
||||
|
||||
const accounts = response.data.map(({ account }) => account);
|
||||
const accountIds = accounts.map((account) => account.id);
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
dispatch(fetchRelationships(accountIds));
|
||||
|
||||
return {
|
||||
result: response.data.map(x => ({ ...x, account: x.account.id })),
|
||||
link: nextLink,
|
||||
hasMore,
|
||||
};
|
||||
};
|
||||
|
||||
const getTruthSuggestions = async (pageParam: PageParam): Promise<PaginatedResult<Result>> => {
|
||||
const endpoint = pageParam?.link || '/api/v1/truth/carousels/suggestions';
|
||||
const response = await api.get<TruthSuggestion[]>(endpoint);
|
||||
const hasMore = !!response.headers.link;
|
||||
const nextLink = getLinks(response).refs.find(link => link.rel === 'next')?.uri;
|
||||
|
||||
const accounts = response.data.map(mapSuggestedProfileToAccount);
|
||||
dispatch(importFetchedAccounts(accounts, { should_refetch: true }));
|
||||
|
||||
return {
|
||||
result: response.data.map((x) => ({ ...x, account: x.account_id })),
|
||||
link: nextLink,
|
||||
hasMore,
|
||||
};
|
||||
};
|
||||
|
||||
const getSuggestions = (pageParam: PageParam) => {
|
||||
if (features.truthSuggestions) {
|
||||
return getTruthSuggestions(pageParam);
|
||||
} else {
|
||||
return getV2Suggestions(pageParam);
|
||||
}
|
||||
};
|
||||
|
||||
const result = useInfiniteQuery(
|
||||
suggestionKeys.suggestions,
|
||||
({ pageParam }: any) => getSuggestions(pageParam),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
getNextPageParam: (config) => {
|
||||
if (config?.hasMore) {
|
||||
return { nextLink: config?.link };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const data: any = result.data?.pages.reduce<Suggestion[]>(
|
||||
(prev: any, curr: any) => [...prev, ...curr.result],
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: data || [],
|
||||
};
|
||||
};
|
||||
|
||||
const useDismissSuggestion = () => {
|
||||
const api = useApi();
|
||||
|
||||
return useMutation((accountId: string) => api.delete(`/api/v1/suggestions/${accountId}`), {
|
||||
onMutate(accountId: string) {
|
||||
removePageItem(suggestionKeys.suggestions, accountId, (o: any, n: any) => o.account_id === n);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function useOnboardingSuggestions() {
|
||||
const api = useApi();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getV2Suggestions = async(pageParam: any): Promise<{ data: Suggestion[], link: string | undefined, hasMore: boolean }> => {
|
||||
const getV2Suggestions = async (pageParam: any): Promise<{ data: Suggestion[], link: string | undefined, hasMore: boolean }> => {
|
||||
const link = pageParam?.link || '/api/v2/suggestions';
|
||||
const response = await api.get<Suggestion[]>(link);
|
||||
const hasMore = !!response.headers.link;
|
||||
|
@ -55,3 +171,5 @@ export default function useOnboardingSuggestions() {
|
|||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export { useOnboardingSuggestions, useSuggestions, useDismissSuggestion };
|
|
@ -1,7 +1,9 @@
|
|||
/** List of supported E164 country codes. */
|
||||
const COUNTRY_CODES = [
|
||||
'1',
|
||||
'351',
|
||||
'44',
|
||||
'55',
|
||||
] as const;
|
||||
|
||||
/** Supported E164 country code. */
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import { queryClient } from 'soapbox/queries/client';
|
||||
|
||||
import type { InfiniteData, QueryKey, UseInfiniteQueryResult } from '@tanstack/react-query';
|
||||
import type { PaginatedResult } from 'soapbox/queries/chats';
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
result: T[],
|
||||
hasMore: boolean,
|
||||
link?: string,
|
||||
}
|
||||
|
||||
/** Flatten paginated results into a single array. */
|
||||
const flattenPages = <T>(queryInfo: UseInfiniteQueryResult<PaginatedResult<T>>) => {
|
||||
|
@ -35,8 +40,22 @@ const appendPageItem = <T>(queryKey: QueryKey, newItem: T) => {
|
|||
});
|
||||
};
|
||||
|
||||
/** Remove an item inside if found. */
|
||||
const removePageItem = <T>(queryKey: QueryKey, itemToRemove: 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.filter(item => !isItem(item, itemToRemove));
|
||||
return { ...page, result };
|
||||
});
|
||||
return { ...data, pages };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
flattenPages,
|
||||
updatePageItem,
|
||||
appendPageItem,
|
||||
};
|
||||
removePageItem,
|
||||
};
|
||||
|
|
Ładowanie…
Reference in New Issue