pl-fe: migrate account actions to tanstack query

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-10-23 18:25:50 +02:00
parent ce379e8621
commit 08d7c8dfba
16 changed files with 328 additions and 322 deletions

View File

@@ -12,7 +12,6 @@ import {
fetchAccountByUsername,
fetchRelationships,
muteAccount,
removeFromFollowers,
unblockAccount,
unmuteAccount,
} from './accounts';
@@ -775,72 +774,6 @@ describe('unsubscribeAccount()', () => {
});
});
describe('removeFromFollowers()', () => {
const id = '1';
describe('when logged out', () => {
beforeEach(() => {
const state = { ...rootState, me: null };
store = mockStore(state);
});
it('should do nothing', async() => {
await store.dispatch(removeFromFollowers(id));
const actions = store.getActions();
expect(actions).toEqual([]);
});
});
describe('when logged in', () => {
beforeEach(() => {
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
describe('with a successful API request', () => {
beforeEach(() => {
__stub((mock) => {
mock.onPost(`/api/v1/accounts/${id}/remove_from_followers`).reply(200, {});
});
});
it('should dispatch the correct actions', async() => {
const expectedActions = [
{ type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST', id },
{
type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS',
relationship: {},
},
];
await store.dispatch(removeFromFollowers(id));
const actions = store.getActions();
expect(actions).toEqual(expectedActions);
});
});
describe('with an unsuccessful API request', () => {
beforeEach(() => {
__stub((mock) => {
mock.onPost(`/api/v1/accounts/${id}/remove_from_followers`).networkError();
});
});
it('should dispatch the correct actions', async() => {
const expectedActions = [
{ type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST', id },
{ type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL', id, error: new Error('Network Error') },
];
await store.dispatch(removeFromFollowers(id));
const actions = store.getActions();
expect(actions).toEqual(expectedActions);
});
});
});
});
describe('fetchRelationships()', () => {
const id = '1';

View File

@@ -1,9 +1,4 @@
import {
type UpdateNotificationSettingsParams,
type CreateAccountParams,
type Relationship,
type MuteAccountParams,
} from 'pl-api';
import { type CreateAccountParams, type Relationship } from 'pl-api';
import { queryClient } from 'pl-fe/queries/client';
import { selectAccount } from 'pl-fe/selectors';
@@ -13,13 +8,11 @@ import { getClient, type PlfeResponse } from '../api';
import { importEntities } from './importer';
import type { MinifiedSuggestion } from 'pl-fe/queries/trends/use-suggested-accounts';
import type { MinifiedStatus } from 'pl-fe/reducers/statuses';
import type { AppDispatch, RootState } from 'pl-fe/store';
import type { History } from 'pl-fe/types/history';
const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS' as const;
const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS' as const;
const maybeRedirectLogin = (error: { response: PlfeResponse }, history?: History) => {
@@ -29,8 +22,6 @@ const maybeRedirectLogin = (error: { response: PlfeResponse }, history?: History
}
};
const noOp = () => new Promise(f => f(undefined));
const createAccount = (params: CreateAccountParams) =>
async (dispatch: AppDispatch, getState: () => RootState) =>
getClient(getState()).settings.createAccount(params).then((response) =>
@@ -84,88 +75,6 @@ const fetchAccountByUsername = (username: string, history?: History) =>
}
};
const blockAccount = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return null;
return getClient(getState).filtering.blockAccount(accountId)
.then(response => {
dispatch(importEntities({ relationships: [response] }));
queryClient.setQueryData<Array<MinifiedSuggestion>>(['suggestions'], suggestions => suggestions
? suggestions.filter((suggestion) => suggestion.account_id !== accountId)
: undefined);
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
return dispatch(blockAccountSuccess(response, getState().statuses));
});
};
const unblockAccount = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return null;
return getClient(getState).filtering.unblockAccount(accountId)
.then(response => {
dispatch(importEntities({ relationships: [response] }));
});
};
const blockAccountSuccess = (relationship: Relationship, statuses: Record<string, MinifiedStatus>) => ({
type: ACCOUNT_BLOCK_SUCCESS,
relationship,
statuses,
});
const muteAccount = (accountId: string, notifications?: boolean, duration = 0) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return null;
const client = getClient(getState);
const params: MuteAccountParams = {
notifications,
};
if (duration) {
params.duration = duration;
}
return client.filtering.muteAccount(accountId, params)
.then(response => {
dispatch(importEntities({ relationships: [response] }));
queryClient.setQueryData<Array<MinifiedSuggestion>>(['suggestions'], suggestions => suggestions
? suggestions.filter((suggestion) => suggestion.account_id !== accountId)
: undefined);
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
return dispatch(muteAccountSuccess(response, getState().statuses));
});
};
const unmuteAccount = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return null;
return getClient(getState()).filtering.unmuteAccount(accountId)
.then(response => dispatch(importEntities({ relationships: [response] })));
};
const muteAccountSuccess = (relationship: Relationship, statuses: Record<string, MinifiedStatus>) => ({
type: ACCOUNT_MUTE_SUCCESS,
relationship,
statuses,
});
const removeFromFollowers = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return null;
return getClient(getState()).accounts.removeAccountFromFollowers(accountId)
.then(response => dispatch(importEntities({ relationships: [response] })));
};
const fetchRelationships = (accountIds: string[]) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return null;
@@ -180,28 +89,6 @@ const fetchRelationships = (accountIds: string[]) =>
.then(response => dispatch(importEntities({ relationships: response })));
};
const pinAccount = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return dispatch(noOp);
return getClient(getState).accounts.pinAccount(accountId).then(response =>
dispatch(importEntities({ relationships: [response] })),
);
};
const unpinAccount = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return dispatch(noOp);
return getClient(getState).accounts.unpinAccount(accountId).then(response =>
dispatch(importEntities({ relationships: [response] })),
);
};
const updateNotificationSettings = (params: UpdateNotificationSettingsParams) =>
(dispatch: AppDispatch, getState: () => RootState) =>
getClient(getState).settings.updateNotificationSettings(params).then((data) => ({ params, data }));
const accountLookup = (acct: string, signal?: AbortSignal) =>
(dispatch: AppDispatch, getState: () => RootState) =>
getClient(getState()).accounts.lookupAccount(acct, { signal }).then((account) => {
@@ -209,13 +96,11 @@ const accountLookup = (acct: string, signal?: AbortSignal) =>
return account;
});
const biteAccount = (accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) =>
getClient(getState).accounts.biteAccount(accountId);
type AccountsAction =
| ReturnType<typeof blockAccountSuccess>
| ReturnType<typeof muteAccountSuccess>;
type AccountsAction = {
type: typeof ACCOUNT_BLOCK_SUCCESS | typeof ACCOUNT_MUTE_SUCCESS;
relationship: Relationship;
statuses: Record<string, MinifiedStatus>;
};
export {
ACCOUNT_BLOCK_SUCCESS,
@@ -223,16 +108,7 @@ export {
createAccount,
fetchAccount,
fetchAccountByUsername,
blockAccount,
unblockAccount,
muteAccount,
unmuteAccount,
removeFromFollowers,
fetchRelationships,
pinAccount,
unpinAccount,
updateNotificationSettings,
accountLookup,
biteAccount,
type AccountsAction,
};

View File

@@ -158,7 +158,7 @@ const DropdownMenuContent: React.FC<IDropdownMenuContent> = ({ handleClose, item
{Component && <Component handleClose={handleClose} />}
{(items?.length || touchscreen) && renderItems(items)}
</div>
<div className={clsx({ 'w-full': touchscreen, 'fit-content mr-auto': !touchscreen })} style={{ width }}>
<div className={clsx({ 'w-full': touchscreen, 'fit-content mr-auto': !touchscreen })} style={{ width }}>
{tab !== undefined && (
<>
<HStack className='mx-2 my-1 text-gray-700 dark:text-gray-300' space={3} alignItems='center'>

View File

@@ -3,7 +3,6 @@ import React, { useCallback, useMemo } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useHistory, useRouteMatch } from 'react-router-dom';
import { blockAccount } from 'pl-fe/actions/accounts';
import { redactStatus } from 'pl-fe/actions/admin';
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'pl-fe/actions/compose';
import { emojiReact, unEmojiReact } from 'pl-fe/actions/emoji-reacts';
@@ -24,6 +23,7 @@ import { useClient } from 'pl-fe/hooks/use-client';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useInstance } from 'pl-fe/hooks/use-instance';
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
import { useBlockAccountMutation, useUnblockAccountMutation } from 'pl-fe/queries/accounts/use-relationship';
import { useChats } from 'pl-fe/queries/chats';
import { useBlockGroupUserMutation } from 'pl-fe/queries/groups/use-group-blocks';
import { useCustomEmojis } from 'pl-fe/queries/instance/use-custom-emojis';
@@ -50,6 +50,7 @@ const messages = defineMessages({
adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
blocked: { id: 'group.group_mod_block.success', defaultMessage: '@{name} is banned' },
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block and report' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
@@ -579,6 +580,8 @@ const MenuButton: React.FC<IMenuButton> = ({
const { mutate: unbookmarkStatus } = useUnbookmarkStatus(status.id);
const { mutate: pinStatus } = usePinStatus(status?.id!);
const { mutate: unpinStatus } = useUnpinStatus(status?.id!);
const { mutate: blockAccount } = useBlockAccountMutation(status.account_id);
const { mutate: unblockAccount } = useUnblockAccountMutation(status.account_id);
const { groupRelationship } = useGroupRelationship(status.group_id || undefined);
const features = useFeatures();
@@ -691,15 +694,19 @@ const MenuButton: React.FC<IMenuButton> = ({
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.acct }} />,
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.id)),
onConfirm: () => blockAccount(),
secondary: intl.formatMessage(messages.blockAndReport),
onSecondary: () => {
dispatch(blockAccount(account.id));
blockAccount();
dispatch(initReport(ReportableEntities.STATUS, account, { status }));
},
});
};
const handleUnblockClick: React.EventHandler<React.MouseEvent> = (e) => {
unblockAccount();
};
const handleEmbed = () => {
openModal('EMBED', {
url: status.url,
@@ -973,11 +980,19 @@ const MenuButton: React.FC<IMenuButton> = ({
action: handleMuteClick,
icon: require('@phosphor-icons/core/regular/speaker-x.svg'),
});
menu.push({
text: intl.formatMessage(messages.block, { name: username }),
action: handleBlockClick,
icon: require('@phosphor-icons/core/regular/prohibit.svg'),
});
if (status.account.relationship?.blocking) {
menu.push({
text: intl.formatMessage(messages.unblock, { name: username }),
action: handleUnblockClick,
icon: require('@phosphor-icons/core/regular/prohibit.svg'),
});
} else {
menu.push({
text: intl.formatMessage(messages.block, { name: username }),
action: handleBlockClick,
icon: require('@phosphor-icons/core/regular/prohibit.svg'),
});
}
menu.push({
text: intl.formatMessage(messages.report, { name: username }),
action: handleReport,

View File

@@ -6,7 +6,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import * as v from 'valibot';
import { biteAccount, blockAccount, pinAccount, removeFromFollowers, unblockAccount, unmuteAccount, unpinAccount } from 'pl-fe/actions/accounts';
import { mentionCompose, directCompose } from 'pl-fe/actions/compose';
import { initReport, ReportableEntities } from 'pl-fe/actions/reports';
import Account from 'pl-fe/components/account';
@@ -29,7 +28,15 @@ import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useClient } from 'pl-fe/hooks/use-client';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
import { useFollowMutation } from 'pl-fe/queries/accounts/use-relationship';
import {
useBlockAccountMutation,
useFollowAccountMutation,
usePinAccountMutation,
useRemoveAccountFromFollowersMutation,
useUnblockAccountMutation,
useUnmuteAccountMutation,
useUnpinAccountMutation,
} from 'pl-fe/queries/accounts/use-relationship';
import { useChats } from 'pl-fe/queries/chats';
import { queryClient } from 'pl-fe/queries/client';
import { blockDomainMutationOptions, unblockDomainMutationOptions } from 'pl-fe/queries/settings/domain-blocks';
@@ -134,7 +141,13 @@ const Header: React.FC<IHeader> = ({ account }) => {
const features = useFeatures();
const { account: ownAccount } = useOwnAccount();
const { mutate: follow } = useFollowMutation(account?.id!);
const { mutate: followAccount } = useFollowAccountMutation(account?.id!);
const { mutate: blockAccount } = useBlockAccountMutation(account?.id!);
const { mutate: unblockAccount } = useUnblockAccountMutation(account?.id!);
const { mutate: unmuteAccount } = useUnmuteAccountMutation(account?.id!);
const { mutate: pinAccount } = usePinAccountMutation(account?.id!);
const { mutate: unpinAccount } = useUnpinAccountMutation(account?.id!);
const { mutate: removeFromFollowers } = useRemoveAccountFromFollowersMutation(account?.id!);
const { openModal } = useModalsActions();
const settings = useSettings();
@@ -181,16 +194,16 @@ const Header: React.FC<IHeader> = ({ account }) => {
const onBlock = () => {
if (account.relationship?.blocking) {
dispatch(unblockAccount(account.id));
unblockAccount();
} else {
openModal('CONFIRM', {
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.acct }} />,
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.id)),
onConfirm: () => blockAccount(),
secondary: intl.formatMessage(messages.blockAndReport),
onSecondary: () => {
dispatch(blockAccount(account.id));
blockAccount();
dispatch(initReport(ReportableEntities.ACCOUNT, account));
},
});
@@ -207,26 +220,26 @@ const Header: React.FC<IHeader> = ({ account }) => {
const onReblogToggle = () => {
if (account.relationship?.showing_reblogs) {
follow({ reblogs: false });
followAccount({ reblogs: false });
} else {
follow({ reblogs: true });
followAccount({ reblogs: true });
}
};
const onEndorseToggle = () => {
if (account.relationship?.endorsed) {
dispatch(unpinAccount(account.id))
.then(() => toast.success(intl.formatMessage(messages.userUnendorsed, { acct: account.acct })))
.catch(() => { });
unpinAccount(undefined, {
onSuccess: () => toast.success(intl.formatMessage(messages.userUnendorsed, { acct: account.acct })),
});
} else {
dispatch(pinAccount(account.id))
.then(() => toast.success(intl.formatMessage(messages.userEndorsed, { acct: account.acct })))
.catch(() => { });
pinAccount(undefined, {
onSuccess: () => toast.success(intl.formatMessage(messages.userEndorsed, { acct: account.acct })),
});
}
};
const onBite = () => {
dispatch(biteAccount(account.id))
client.accounts.biteAccount(account.id)
.then(() => toast.success(intl.formatMessage(messages.userBit, { acct: account.acct })))
.catch(() => toast.error(intl.formatMessage(messages.userBiteFail, { acct: account.acct })));
};
@@ -243,7 +256,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
const onMute = () => {
if (account.relationship?.muting) {
dispatch(unmuteAccount(account.id));
unmuteAccount();
} else {
openModal('MUTE', { accountId: account.id });
}
@@ -279,10 +292,10 @@ const Header: React.FC<IHeader> = ({ account }) => {
heading: <FormattedMessage id='confirmations.remove_from_followers.heading' defaultMessage='Remove {name} from followers' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />,
message: <FormattedMessage id='confirmations.remove_from_followers.message' defaultMessage='Are you sure you want to remove {name} from your followers?' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />,
confirm: intl.formatMessage(messages.removeFromFollowersConfirm),
onConfirm: () => dispatch(removeFromFollowers(account.id)),
onConfirm: () => removeFromFollowers(),
});
} else {
dispatch(removeFromFollowers(account.id));
removeFromFollowers();
}
};

View File

@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { defineMessages, IntlShape, useIntl } from 'react-intl';
import { unblockAccount } from 'pl-fe/actions/accounts';
import Button from 'pl-fe/components/ui/button';
import Combobox, { ComboboxInput, ComboboxList, ComboboxOption, ComboboxPopover } from 'pl-fe/components/ui/combobox';
import HStack from 'pl-fe/components/ui/hstack';
@@ -11,9 +10,8 @@ import Text from 'pl-fe/components/ui/text';
import { useChatContext } from 'pl-fe/contexts/chat-context';
import UploadButton from 'pl-fe/features/compose/components/upload-button';
import emojiSearch from 'pl-fe/features/emoji/search';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useInstance } from 'pl-fe/hooks/use-instance';
import { useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
import { useRelationshipQuery, useUnblockAccountMutation } from 'pl-fe/queries/accounts/use-relationship';
import { useModalsActions } from 'pl-fe/stores/modals';
import { textAtCursorMatchesToken } from 'pl-fe/utils/suggestions';
@@ -76,11 +74,11 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
uploadProgress,
}, ref) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { openModal } = useModalsActions();
const { chat } = useChatContext();
const { data: relationship } = useRelationshipQuery(chat?.account.id);
const { mutate: unblockAccount } = useUnblockAccountMutation(chat?.account.id!);
const isBlocked = relationship?.blocked_by && false;
const isBlocking = relationship?.blocking && false;
@@ -146,7 +144,7 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
message: intl.formatMessage(messages.unblockMessage),
confirm: intl.formatMessage(messages.unblockConfirm),
confirmationTheme: 'primary',
onConfirm: () => dispatch(unblockAccount(chat?.account.id as string)),
onConfirm: () => unblockAccount(),
});
};

View File

@@ -2,7 +2,6 @@ import React, { useRef } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Link, useHistory, useParams } from 'react-router-dom';
import { blockAccount, unblockAccount } from 'pl-fe/actions/accounts';
import DropdownMenu, { type Menu } from 'pl-fe/components/dropdown-menu';
import Avatar from 'pl-fe/components/ui/avatar';
import HStack from 'pl-fe/components/ui/hstack';
@@ -11,9 +10,8 @@ import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import VerificationBadge from 'pl-fe/components/verification-badge';
import { useChatContext } from 'pl-fe/contexts/chat-context';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
import { useBlockAccountMutation, useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
import { useChat, useChatActions, useChats } from 'pl-fe/queries/chats';
import { useModalsActions } from 'pl-fe/stores/modals';
@@ -38,7 +36,6 @@ const messages = defineMessages({
});
const ChatPageMain = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const features = useFeatures();
const history = useHistory();
@@ -50,6 +47,9 @@ const ChatPageMain = () => {
const { currentChatId } = useChatContext();
const { chatsQuery: { data: chats, isLoading } } = useChats();
const { mutate: blockAccount } = useBlockAccountMutation(chat?.account.id!);
const { mutate: unblockAccount } = useUnblockAccountMutation(chat?.account.id!);
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const { deleteChat } = useChatActions(chat?.id as string);
@@ -62,7 +62,7 @@ const ChatPageMain = () => {
message: intl.formatMessage(messages.blockMessage),
confirm: intl.formatMessage(messages.blockConfirm),
confirmationTheme: 'primary',
onConfirm: () => dispatch(blockAccount(chat?.account.id as string)),
onConfirm: () => blockAccount(),
});
};
@@ -72,7 +72,7 @@ const ChatPageMain = () => {
message: intl.formatMessage(messages.unblockMessage),
confirm: intl.formatMessage(messages.unblockConfirm),
confirmationTheme: 'primary',
onConfirm: () => dispatch(unblockAccount(chat?.account.id as string)),
onConfirm: () => unblockAccount(),
});
};

View File

@@ -1,16 +1,14 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { blockAccount, unblockAccount } from 'pl-fe/actions/accounts';
import Avatar from 'pl-fe/components/ui/avatar';
import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import { ChatWidgetScreens, useChatContext } from 'pl-fe/contexts/chat-context';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
import { useBlockAccountMutation, useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
import { useChatActions } from 'pl-fe/queries/chats';
import { useModalsActions } from 'pl-fe/stores/modals';
@@ -33,7 +31,6 @@ const messages = defineMessages({
});
const ChatSettings = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const features = useFeatures();
@@ -41,6 +38,9 @@ const ChatSettings = () => {
const { chat, changeScreen, toggleChatPane } = useChatContext();
const { deleteChat } = useChatActions(chat?.id as string);
const { mutate: blockAccount } = useBlockAccountMutation(chat?.account.id!);
const { mutate: unblockAccount } = useUnblockAccountMutation(chat?.account.id!);
const isBlocked = !!useRelationshipQuery(chat?.account.id).data?.blocked_by;
const closeSettings = () => {
@@ -58,7 +58,7 @@ const ChatSettings = () => {
message: intl.formatMessage(messages.blockMessage),
confirm: intl.formatMessage(messages.blockConfirm),
confirmationTheme: 'primary',
onConfirm: () => dispatch(blockAccount(chat?.account.id as string)),
onConfirm: () => blockAccount(),
});
};
@@ -68,7 +68,7 @@ const ChatSettings = () => {
message: intl.formatMessage(messages.unblockMessage),
confirm: intl.formatMessage(messages.unblockConfirm),
confirmationTheme: 'primary',
onConfirm: () => dispatch(unblockAccount(chat?.account.id as string)),
onConfirm: () => unblockAccount(),
});
};

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link, useHistory } from 'react-router-dom';
import { blockAccount } from 'pl-fe/actions/accounts';
import { directCompose, mentionCompose, quoteCompose } from 'pl-fe/actions/compose';
import { fetchEventIcs } from 'pl-fe/actions/events';
import { deleteStatusModal, toggleStatusSensitivityModal } from 'pl-fe/actions/moderation';
@@ -21,6 +20,7 @@ import Emojify from 'pl-fe/features/emoji/emojify';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
import { useBlockAccountMutation } from 'pl-fe/queries/accounts/use-relationship';
import { useChats } from 'pl-fe/queries/chats';
import { useBookmarkStatus, usePinStatus, useReblogStatus, useUnbookmarkStatus, useUnpinStatus, useUnreblogStatus } from 'pl-fe/queries/statuses/use-status-interactions';
import { useModalsActions } from 'pl-fe/stores/modals';
@@ -95,6 +95,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
const { mutate: unbookmarkStatus } = useUnbookmarkStatus(status?.id!);
const { mutate: pinStatus } = usePinStatus(status?.id!);
const { mutate: unpinStatus } = useUnpinStatus(status?.id!);
const { mutate: blockAccount } = useBlockAccountMutation(status?.account.id!);
if (!status || !status.event) {
return (
@@ -191,10 +192,10 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.acct }} />,
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.acct}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.id)),
onConfirm: () => blockAccount(),
secondary: intl.formatMessage(messages.blockAndReport),
onSecondary: () => {
dispatch(blockAccount(account.id));
blockAccount();
dispatch(initReport(ReportableEntities.STATUS, account, { status }));
},
});

View File

@@ -1,21 +1,22 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import {
blockAccount,
unblockAccount,
muteAccount,
unmuteAccount,
biteAccount,
} from 'pl-fe/actions/accounts';
import Button from 'pl-fe/components/ui/button';
import HStack from 'pl-fe/components/ui/hstack';
import Spinner from 'pl-fe/components/ui/spinner';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useClient } from 'pl-fe/hooks/use-client';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useLoggedIn } from 'pl-fe/hooks/use-logged-in';
import { useAcceptFollowRequestMutation, useRejectFollowRequestMutation } from 'pl-fe/queries/accounts/use-follow-requests';
import { useRelationshipQuery, useFollowMutation, useUnfollowMutation } from 'pl-fe/queries/accounts/use-relationship';
import {
useRelationshipQuery,
useBlockAccountMutation,
useUnblockAccountMutation,
useMuteAccountMutation,
useUnmuteAccountMutation,
useFollowAccountMutation,
useUnfollowAccountMutation,
} from 'pl-fe/queries/accounts/use-relationship';
import { useModalsActions } from 'pl-fe/stores/modals';
import toast from 'pl-fe/toast';
@@ -55,15 +56,19 @@ interface IActionButton {
* `actionType` prop.
*/
const ActionButton: React.FC<IActionButton> = ({ account, actionType, small = true }) => {
const dispatch = useAppDispatch();
const features = useFeatures();
const intl = useIntl();
const client = useClient();
const { openModal } = useModalsActions();
const { isLoggedIn, me } = useLoggedIn();
const { mutate: follow, isPending: isPendingFollow } = useFollowMutation(account.id);
const { mutate: unfollow, isPending: isPendingUnfollow } = useUnfollowMutation(account.id);
const { mutate: followAccount, isPending: isPendingFollow } = useFollowAccountMutation(account.id);
const { mutate: unfollowAccount, isPending: isPendingUnfollow } = useUnfollowAccountMutation(account.id);
const { mutate: blockAccount } = useBlockAccountMutation(account.id);
const { mutate: unblockAccount } = useUnblockAccountMutation(account.id);
const { mutate: muteAccount } = useMuteAccountMutation(account.id);
const { mutate: unmuteAccount } = useUnmuteAccountMutation(account.id);
const { data: relationship, isLoading } = useRelationshipQuery(account.id);
@@ -72,25 +77,25 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small = tr
const handleFollow = () => {
if (relationship?.following || relationship?.requested) {
unfollow();
unfollowAccount();
} else {
follow(undefined);
followAccount(undefined);
}
};
const handleBlock = () => {
if (relationship?.blocking) {
dispatch(unblockAccount(account.id));
unblockAccount();
} else {
dispatch(blockAccount(account.id));
blockAccount();
}
};
const handleMute = () => {
if (relationship?.muting) {
dispatch(unmuteAccount(account.id));
unmuteAccount();
} else {
dispatch(muteAccount(account.id));
muteAccount(undefined);
}
};
@@ -103,7 +108,7 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small = tr
};
const handleBite = () => {
dispatch(biteAccount(account.id))
client.accounts.biteAccount(account.id)
.then(() => toast.success(intl.formatMessage(messages.userBit, { acct: account.acct })))
.catch(() => toast.error(intl.formatMessage(messages.userBiteFail, { acct: account.acct })));
};

View File

@@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
import IconButton from 'pl-fe/components/ui/icon-button';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useFollowMutation } from 'pl-fe/queries/accounts/use-relationship';
import { useFollowAccountMutation } from 'pl-fe/queries/accounts/use-relationship';
import toast from 'pl-fe/toast';
import type { Account as AccountEntity } from 'pl-fe/normalizers/account';
@@ -24,7 +24,7 @@ interface ISubscriptionButton {
const SubscriptionButton = ({ account }: ISubscriptionButton) => {
const features = useFeatures();
const intl = useIntl();
const { mutate: follow, isPending } = useFollowMutation(account.id);
const { mutate: follow, isPending } = useFollowAccountMutation(account.id);
const isFollowing = account.relationship?.following;
const isRequested = account.relationship?.requested;

View File

@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { muteAccount } from 'pl-fe/actions/accounts';
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
import HStack from 'pl-fe/components/ui/hstack';
import Modal from 'pl-fe/components/ui/modal';
@@ -9,8 +8,8 @@ import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import Toggle from 'pl-fe/components/ui/toggle';
import DurationSelector from 'pl-fe/features/compose/components/polls/duration-selector';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useMuteAccountMutation } from 'pl-fe/queries/accounts/use-relationship';
import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root';
@@ -19,21 +18,23 @@ interface MuteModalProps {
}
const MuteModal: React.FC<MuteModalProps & BaseModalProps> = ({ accountId, onClose }) => {
const dispatch = useAppDispatch();
const { account } = useAccount(accountId || undefined);
const [notifications, setNotifications] = useState(true);
const [duration, setDuration] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const mutesDuration = useFeatures().mutesDuration;
const { mutate: muteAccount } = useMuteAccountMutation(accountId);
if (!account) return null;
const handleClick = () => {
setIsSubmitting(true);
dispatch(muteAccount(account.id, notifications, duration))?.then(() => {
setIsSubmitting(false);
onClose('MUTE');
muteAccount({ notifications, duration }, {
onSuccess: () => {
setIsSubmitting(false);
onClose('MUTE');
},
});
};

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { blockAccount } from 'pl-fe/actions/accounts';
import { submitReport, ReportableEntities } from 'pl-fe/actions/reports';
import { fetchAccountTimeline } from 'pl-fe/actions/timelines';
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
@@ -15,6 +14,7 @@ import AccountContainer from 'pl-fe/containers/account-container';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useInstance } from 'pl-fe/hooks/use-instance';
import { useBlockAccountMutation } from 'pl-fe/queries/accounts/use-relationship';
import ConfirmationStep from './steps/confirmation-step';
import OtherActionsStep from './steps/other-actions-step';
@@ -81,6 +81,8 @@ const ReportModal: React.FC<BaseModalProps & ReportModalProps> = ({ onClose, acc
const { account } = useAccount(accountId || undefined);
const { mutate: blockAccount } = useBlockAccountMutation(accountId);
const [block, setBlock] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const { rules } = useInstance();
@@ -109,7 +111,7 @@ const ReportModal: React.FC<BaseModalProps & ReportModalProps> = ({ onClose, acc
});
if (block && account) {
dispatch(blockAccount(account.id));
blockAccount();
}
};

View File

@@ -175,7 +175,7 @@ const ReportPage: React.FC<IReportPage> = (props) => {
<td className='p-2.5 text-end'>
<Text size='sm'>
<FormattedDate value={report.created_at} year='2-digit' month='short' day='2-digit' weekday='short' />
<FormattedDate value={report.created_at} year='2-digit' month='short' day='2-digit' weekday='short' />
</Text>
</td>
</tr>

View File

@@ -3,7 +3,6 @@ import { type CredentialAccount, GOTOSOCIAL } from 'pl-api';
import React, { useState, useEffect } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { updateNotificationSettings } from 'pl-fe/actions/accounts';
import { patchMe } from 'pl-fe/actions/me';
import BirthdayInput from 'pl-fe/components/birthday-input';
import List, { ListItem } from 'pl-fe/components/list';
@@ -244,9 +243,9 @@ const EditProfilePage: React.FC = () => {
if (features.muteStrangers) {
promises.push(
dispatch(updateNotificationSettings({
client.settings.updateNotificationSettings({
block_from_strangers: muteStrangers,
})).catch(console.error),
}).catch(console.error),
);
}

View File

@@ -1,9 +1,32 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, type AccountsAction } from 'pl-fe/actions/accounts';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useClient } from 'pl-fe/hooks/use-client';
import { useLoggedIn } from 'pl-fe/hooks/use-logged-in';
import type { FollowAccountParams, Relationship } from 'pl-api';
import type { MinifiedSuggestion } from '../trends/use-suggested-accounts';
import type { FollowAccountParams, MuteAccountParams, Relationship } from 'pl-api';
const updateRelationship = (accountId: string, changes: Partial<Relationship> | ((relationship: Relationship) => Relationship), queryClient: ReturnType<typeof useQueryClient>) => {
const previousRelationship = queryClient.getQueryData<Relationship>(['accountRelationships', accountId]);
if (!previousRelationship) return;
const newRelationship = typeof changes === 'function' ? changes(previousRelationship) : { ...previousRelationship, ...changes };
queryClient.setQueryData(['accountRelationships', accountId], newRelationship);
return { previousRelationship };
};
const restorePreviousRelationship = (
accountId: string,
context: { previousRelationship?: Relationship } | undefined,
queryClient: ReturnType<typeof useQueryClient>,
) => {
if (context?.previousRelationship) {
queryClient.setQueryData(['accountRelationships', accountId], context.previousRelationship);
}
};
const useRelationshipQuery = (accountId?: string) => {
const client = useClient();
@@ -16,7 +39,7 @@ const useRelationshipQuery = (accountId?: string) => {
});
};
const useFollowMutation = (accountId: string) => {
const useFollowAccountMutation = (accountId: string) => {
const client = useClient();
const queryClient = useQueryClient();
@@ -24,64 +47,204 @@ const useFollowMutation = (accountId: string) => {
mutationKey: ['accountRelationships', accountId],
mutationFn: (params?: FollowAccountParams) => client.accounts.followAccount(accountId, params),
onMutate: (params) => {
const previousRelationship = queryClient.getQueryData<Relationship>(['accountRelationships', accountId])!;
if (!previousRelationship) return;
const newRelationship: Relationship = {
...previousRelationship,
requested: !previousRelationship.following,
notifying: params?.notify ?? previousRelationship.notifying,
showing_reblogs: params?.reblogs ?? previousRelationship.showing_reblogs,
};
queryClient.setQueryData(['accountRelationships', accountId], newRelationship);
return { previousRelationship };
},
onError: (_err, _variables, context) => {
if (context?.previousRelationship) {
queryClient.setQueryData(['accountRelationships', accountId], context.previousRelationship);
}
return updateRelationship(accountId, (relationship) => ({
...relationship,
requested: !relationship.following,
notifying: params?.notify ?? relationship.notifying,
showing_reblogs: params?.reblogs ?? relationship.showing_reblogs,
}), queryClient);
},
onError: (_err, _variables, context) => restorePreviousRelationship(accountId, context, queryClient),
onSuccess: (data) => {
queryClient.setQueryData(['accountRelationships', accountId], data);
},
});
};
const useUnfollowMutation = (accountId: string) => {
const useUnfollowAccountMutation = (accountId: string) => {
const client = useClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['accountRelationships', accountId],
mutationFn: () => client.accounts.unfollowAccount(accountId),
onMutate: () => {
const previousRelationship = queryClient.getQueryData<Relationship>(['accountRelationships', accountId])!;
if (!previousRelationship) return;
const newRelationship: Relationship = {
...previousRelationship,
following: false,
requested: false,
notifying: false,
showing_reblogs: false,
};
queryClient.setQueryData(['accountRelationships', accountId], newRelationship);
return { previousRelationship };
},
onError: (_err, _variables, context) => {
if (context?.previousRelationship) {
queryClient.setQueryData(['accountRelationships', accountId], context.previousRelationship);
}
},
onMutate: () => updateRelationship(accountId, {
following: false,
requested: false,
notifying: false,
showing_reblogs: false,
}, queryClient),
onError: (_err, _variables, context) => restorePreviousRelationship(accountId, context, queryClient),
onSuccess: (data) => {
queryClient.setQueryData(['accountRelationships', accountId], data);
},
});
};
export { useRelationshipQuery, useFollowMutation, useUnfollowMutation };
const useBlockAccountMutation = (accountId: string) => {
const client = useClient();
const queryClient = useQueryClient();
const dispatch = useAppDispatch();
return useMutation({
mutationKey: ['accountRelationships', accountId],
mutationFn: () => client.filtering.blockAccount(accountId),
onMutate: () => updateRelationship(accountId, {
blocking: true,
followed_by: false,
following: false,
notifying: false,
requested: false,
}, queryClient),
onError: (_err, _variables, context) => restorePreviousRelationship(accountId, context, queryClient),
onSuccess: (data) => {
queryClient.setQueryData(['accountRelationships', accountId], data);
queryClient.setQueryData<Array<MinifiedSuggestion>>(['suggestions'], suggestions => suggestions
? suggestions.filter((suggestion) => suggestion.account_id !== accountId)
: undefined);
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
return dispatch<AccountsAction>((dispatch, getState) => dispatch({
type: ACCOUNT_BLOCK_SUCCESS,
relationship: data,
statuses: getState().statuses,
}));
},
});
};
const useUnblockAccountMutation = (accountId: string) => {
const client = useClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['accountRelationships', accountId],
mutationFn: () => client.filtering.unblockAccount(accountId),
onMutate: () => updateRelationship(accountId, {
blocking: true,
}, queryClient),
onError: (_err, _variables, context) => restorePreviousRelationship(accountId, context, queryClient),
onSuccess: (data) => {
queryClient.setQueryData(['accountRelationships', accountId], data);
},
});
};
const useMuteAccountMutation = (accountId: string) => {
const client = useClient();
const queryClient = useQueryClient();
const dispatch = useAppDispatch();
return useMutation({
mutationKey: ['accountRelationships', accountId],
mutationFn: (params?: MuteAccountParams) => client.filtering.muteAccount(accountId, params),
onMutate: () => updateRelationship(accountId, {
muting: true,
}, queryClient),
onError: (_err, _variables, context) => restorePreviousRelationship(accountId, context, queryClient),
onSuccess: (data) => {
queryClient.setQueryData(['accountRelationships', accountId], data);
queryClient.setQueryData<Array<MinifiedSuggestion>>(['suggestions'], suggestions => suggestions
? suggestions.filter((suggestion) => suggestion.account_id !== accountId)
: undefined);
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
return dispatch<AccountsAction>((dispatch, getState) => dispatch({
type: ACCOUNT_MUTE_SUCCESS,
relationship: data,
statuses: getState().statuses,
}));
},
});
};
const useUnmuteAccountMutation = (accountId: string) => {
const client = useClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['accountRelationships', accountId],
mutationFn: () => client.filtering.unmuteAccount(accountId),
onMutate: () => updateRelationship(accountId, {
muting: false,
}, queryClient),
onError: (_err, _variables, context) => restorePreviousRelationship(accountId, context, queryClient),
onSuccess: (data) => {
queryClient.setQueryData(['accountRelationships', accountId], data);
},
});
};
const usePinAccountMutation = (accountId: string) => {
const client = useClient();
const queryClient = useQueryClient();
const { me } = useLoggedIn();
return useMutation({
mutationKey: ['accountRelationships', accountId],
mutationFn: () => client.accounts.pinAccount(accountId),
onMutate: () => updateRelationship(accountId, {
endorsed: true,
}, queryClient),
onError: (_err, _variables, context) => restorePreviousRelationship(accountId, context, queryClient),
onSuccess: (data) => {
queryClient.setQueryData(['accountRelationships', accountId], data);
queryClient.invalidateQueries({
queryKey: ['accountsLists', 'endorsedAccounts', me],
});
},
});
};
const useUnpinAccountMutation = (accountId: string) => {
const client = useClient();
const queryClient = useQueryClient();
const { me } = useLoggedIn();
return useMutation({
mutationKey: ['accountRelationships', accountId],
mutationFn: () => client.accounts.unpinAccount(accountId),
onMutate: () => updateRelationship(accountId, {
endorsed: false,
}, queryClient),
onError: (_err, _variables, context) => restorePreviousRelationship(accountId, context, queryClient),
onSuccess: (data) => {
queryClient.setQueryData(['accountRelationships', accountId], data);
queryClient.invalidateQueries({
queryKey: ['accountsLists', 'endorsedAccounts', me],
});
},
});
};
const useRemoveAccountFromFollowersMutation = (accountId: string) => {
const client = useClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['accountRelationships', accountId],
mutationFn: () => client.accounts.removeAccountFromFollowers(accountId),
onMutate: () => updateRelationship(accountId, {
followed_by: false,
}, queryClient),
onError: (_err, _variables, context) => restorePreviousRelationship(accountId, context, queryClient),
onSuccess: (data) => {
queryClient.setQueryData(['accountRelationships', accountId], data);
},
});
};
export {
useRelationshipQuery,
useFollowAccountMutation,
useUnfollowAccountMutation,
useBlockAccountMutation,
useUnblockAccountMutation,
useMuteAccountMutation,
useUnmuteAccountMutation,
usePinAccountMutation,
useUnpinAccountMutation,
useRemoveAccountFromFollowersMutation,
};