pl-fe: migrate domain blocks

Signed-off-by: mkljczk <git@mkljczk.pl>
This commit is contained in:
mkljczk
2025-03-15 18:38:48 +01:00
parent 9188fcad95
commit 4f7af2256b
8 changed files with 79 additions and 194 deletions

View File

@ -1,109 +0,0 @@
import { Entities } from 'pl-fe/entity-store/entities';
import { queryClient } from 'pl-fe/queries/client';
import { isLoggedIn } from 'pl-fe/utils/auth';
import { getClient } from '../api';
import type { PaginatedResponse } from 'pl-api';
import type { EntityStore } from 'pl-fe/entity-store/types';
import type { Account } from 'pl-fe/normalizers/account';
import type { MinifiedSuggestion } from 'pl-fe/queries/trends/use-suggested-accounts';
import type { AppDispatch, RootState } from 'pl-fe/store';
const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS' as const;
const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS' as const;
const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS' as const;
const blockDomain = (domain: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
return getClient(getState).filtering.blockDomain(domain).then(() => {
// TODO: Update relationships on block
const accounts = selectAccountsByDomain(getState(), domain);
if (!accounts) return;
queryClient.setQueryData<Array<MinifiedSuggestion>>(['suggestions'], suggestions => suggestions
? suggestions.filter((suggestion) => !accounts.includes(suggestion.account_id))
: undefined);
});
};
const unblockDomain = (domain: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
return getClient(getState).filtering.unblockDomain(domain).then(() => {
// TODO: Update relationships on unblock
const accounts = selectAccountsByDomain(getState(), domain);
if (!accounts) return;
dispatch(unblockDomainSuccess(domain, accounts));
}).catch(() => {});
};
const unblockDomainSuccess = (domain: string, accounts: string[]) => ({
type: DOMAIN_UNBLOCK_SUCCESS,
domain,
accounts,
});
const fetchDomainBlocks = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
return getClient(getState).filtering.getDomainBlocks().then(response => {
dispatch(fetchDomainBlocksSuccess(response.items, response.next));
});
};
const fetchDomainBlocksSuccess = (domains: string[], next: (() => Promise<PaginatedResponse<string>>) | null) => ({
type: DOMAIN_BLOCKS_FETCH_SUCCESS,
domains,
next,
});
const expandDomainBlocks = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const next = getState().domain_lists.blocks.next;
if (!next) return;
next().then(response => {
dispatch(expandDomainBlocksSuccess(response.items, response.next));
}).catch(() => {});
};
const selectAccountsByDomain = (state: RootState, domain: string): string[] => {
const store = state.entities[Entities.ACCOUNTS]?.store as EntityStore<Account> | undefined;
const entries = store ? Object.entries(store) : undefined;
const accounts = entries
?.filter(([_, item]) => item && item.acct.endsWith(`@${domain}`))
.map(([_, item]) => item!.id);
return accounts || [];
};
const expandDomainBlocksSuccess = (domains: string[], next: (() => Promise<PaginatedResponse<string>>) | null) => ({
type: DOMAIN_BLOCKS_EXPAND_SUCCESS,
domains,
next,
});
type DomainBlocksAction =
| ReturnType<typeof unblockDomainSuccess>
| ReturnType<typeof fetchDomainBlocksSuccess>
| ReturnType<typeof expandDomainBlocksSuccess>;
export {
DOMAIN_UNBLOCK_SUCCESS,
DOMAIN_BLOCKS_FETCH_SUCCESS,
DOMAIN_BLOCKS_EXPAND_SUCCESS,
blockDomain,
unblockDomain,
fetchDomainBlocks,
expandDomainBlocks,
type DomainBlocksAction,
};

View File

@ -1,11 +1,11 @@
import { useMutation } from '@tanstack/react-query';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { unblockDomain } from 'pl-fe/actions/domain-blocks';
import HStack from 'pl-fe/components/ui/hstack';
import IconButton from 'pl-fe/components/ui/icon-button';
import Text from 'pl-fe/components/ui/text';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { unblockDomainMutationOptions } from 'pl-fe/queries/settings/domain-blocks';
const messages = defineMessages({
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
@ -17,20 +17,21 @@ interface IDomain {
}
const Domain: React.FC<IDomain> = ({ domain }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { mutate: unblockDomain } = useMutation(unblockDomainMutationOptions);
// const onBlockDomain = () => {
// openModal('CONFIRM', {
// heading: <FormattedMessage id='confirmations.domain_block.heading' defaultMessage='Block {domain}' values={{ domain }} />,
// message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />,
// confirm: intl.formatMessage(messages.blockDomainConfirm),
// onConfirm: () => dispatch(blockDomain(domain)),
// onConfirm: () => blockDomain(domain),
// });
// }
const handleDomainUnblock = () => {
dispatch(unblockDomain(domain));
unblockDomain(domain);
};
return (

View File

@ -7,7 +7,6 @@ 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 { blockDomain, unblockDomain } from 'pl-fe/actions/domain-blocks';
import { initReport, ReportableEntities } from 'pl-fe/actions/reports';
import { useFollow } from 'pl-fe/api/hooks/accounts/use-follow';
import Badge from 'pl-fe/components/badge';
@ -26,6 +25,7 @@ import { useFeatures } from 'pl-fe/hooks/use-features';
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
import { useChats } from 'pl-fe/queries/chats';
import { queryClient } from 'pl-fe/queries/client';
import { blockDomainMutationOptions, unblockDomainMutationOptions } from 'pl-fe/queries/settings/domain-blocks';
import { useModalsStore } from 'pl-fe/stores/modals';
import { useSettingsStore } from 'pl-fe/stores/settings';
import toast from 'pl-fe/toast';
@ -100,6 +100,9 @@ const Header: React.FC<IHeader> = ({ account }) => {
const { getOrCreateChatByAccountId } = useChats();
const { mutate: blockDomain } = useMutation(blockDomainMutationOptions);
const { mutate: unblockDomain } = useMutation(unblockDomainMutationOptions);
const createAndNavigateToChat = useMutation({
mutationFn: (accountId: string) => getOrCreateChatByAccountId(accountId),
onError: (error: { response: PlfeResponse }) => {
@ -203,12 +206,12 @@ const Header: React.FC<IHeader> = ({ account }) => {
heading: <FormattedMessage id='confirmations.domain_block.heading' defaultMessage='Block {domain}' values={{ domain }} />,
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
onConfirm: () => blockDomain(domain),
});
};
const onUnblockDomain = (domain: string) => {
dispatch(unblockDomain(domain));
unblockDomain(domain);
};
const onProfileExternal = (url: string) => {

View File

@ -1,34 +1,28 @@
import debounce from 'lodash/debounce';
import { useInfiniteQuery } from '@tanstack/react-query';
import React from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { fetchDomainBlocks, expandDomainBlocks } from 'pl-fe/actions/domain-blocks';
import Domain from 'pl-fe/components/domain';
import ScrollableList from 'pl-fe/components/scrollable-list';
import Column from 'pl-fe/components/ui/column';
import Spinner from 'pl-fe/components/ui/spinner';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { domainBlocksQueryOptions } from 'pl-fe/queries/settings/domain-blocks';
const messages = defineMessages({
heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
});
const handleLoadMore = debounce((dispatch) => {
dispatch(expandDomainBlocks());
}, 300, { leading: true });
const DomainBlocks: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const domains = useAppSelector((state) => state.domain_lists.blocks.items);
const hasMore = useAppSelector((state) => !!state.domain_lists.blocks.next);
const { data: domains, hasNextPage, fetchNextPage } = useInfiniteQuery(domainBlocksQueryOptions);
React.useEffect(() => {
dispatch(fetchDomainBlocks());
}, []);
const handleLoadMore = () => {
if (hasNextPage) {
fetchNextPage({ cancelRefetch: false });
}
};
if (!domains) {
return (
@ -44,8 +38,8 @@ const DomainBlocks: React.FC = () => {
<Column label={intl.formatMessage(messages.heading)}>
<ScrollableList
scrollKey='domainBlocks'
onLoadMore={() => handleLoadMore(dispatch)}
hasMore={hasMore}
onLoadMore={handleLoadMore}
hasMore={hasNextPage}
emptyMessage={emptyMessage}
listClassName='divide-y divide-gray-200 dark:divide-gray-800'
>

View File

@ -0,0 +1,57 @@
import { getClient } from 'pl-fe/api';
import { Entities } from 'pl-fe/entity-store/entities';
import { queryClient } from '../client';
import { makePaginatedResponseQueryOptions } from '../utils/make-paginated-response-query-options';
import { mutationOptions } from '../utils/mutation-options';
import type { MinifiedSuggestion } from '../trends/use-suggested-accounts';
import type { EntityStore } from 'pl-fe/entity-store/types';
import type { Account } from 'pl-fe/normalizers/account';
import type { RootState, Store } from 'pl-fe/store';
let store: Store;
import('pl-fe/store').then((value) => store = value.store).catch(() => {});
const domainBlocksQueryOptions = makePaginatedResponseQueryOptions(
['settings', 'domainBlocks'],
(client) => client.filtering.getDomainBlocks(),
)();
const blockDomainMutationOptions = mutationOptions({
mutationKey: ['settings', 'domainBlocks'],
mutationFn: (domain: string) => getClient().filtering.blockDomain(domain),
onSettled: (_, __, domain) => {
queryClient.invalidateQueries(domainBlocksQueryOptions);
const accounts = selectAccountsByDomain(store.getState(), domain);
if (!accounts) return;
queryClient.setQueryData<Array<MinifiedSuggestion>>(['suggestions'], suggestions => suggestions
? suggestions.filter((suggestion) => !accounts.includes(suggestion.account_id))
: undefined);
},
});
const unblockDomainMutationOptions = mutationOptions({
mutationKey: ['settings', 'domainBlocks'],
mutationFn: (domain: string) => getClient().filtering.unblockDomain(domain),
onSettled: () => {
queryClient.invalidateQueries(domainBlocksQueryOptions);
},
});
const selectAccountsByDomain = (state: RootState, domain: string): string[] => {
const store = state.entities[Entities.ACCOUNTS]?.store as EntityStore<Account> | undefined;
const entries = store ? Object.entries(store) : undefined;
const accounts = entries
?.filter(([_, item]) => item && item.acct.endsWith(`@${domain}`))
.map(([_, item]) => item!.id);
return accounts || [];
};
export {
domainBlocksQueryOptions,
blockDomainMutationOptions,
unblockDomainMutationOptions,
};

View File

@ -1,12 +0,0 @@
import reducer from './domain-lists';
describe('domain_lists reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {} as any).toJS()).toEqual({
blocks: {
items: [],
next: null,
},
});
});
});

View File

@ -1,47 +0,0 @@
import { create } from 'mutative';
import {
DOMAIN_BLOCKS_FETCH_SUCCESS,
DOMAIN_BLOCKS_EXPAND_SUCCESS,
DOMAIN_UNBLOCK_SUCCESS,
type DomainBlocksAction,
} from '../actions/domain-blocks';
import type { PaginatedResponse } from 'pl-api';
interface State {
blocks: {
items: Array<string>;
next: (() => Promise<PaginatedResponse<string>>) | null;
};
}
const initialState: State = {
blocks: {
items: [],
next: null,
},
};
const domainLists = (state: State = initialState, action: DomainBlocksAction): State => {
switch (action.type) {
case DOMAIN_BLOCKS_FETCH_SUCCESS:
return create(state, (draft) => {
draft.blocks.items = action.domains;
draft.blocks.next = action.next;
});
case DOMAIN_BLOCKS_EXPAND_SUCCESS:
return create(state, (draft) => {
draft.blocks.items = [...new Set([...draft.blocks.items, ...action.domains])];
draft.blocks.next = action.next;
});
case DOMAIN_UNBLOCK_SUCCESS:
return create(state, (draft) => {
draft.blocks.items = draft.blocks.items.filter(item => item !== action.domain);
});
default:
return state;
}
};
export { domainLists as default };

View File

@ -12,7 +12,6 @@ import auth from './auth';
import compose from './compose';
import contexts from './contexts';
import conversations from './conversations';
import domain_lists from './domain-lists';
import draft_statuses from './draft-statuses';
import filters from './filters';
import instance from './instance';
@ -41,7 +40,6 @@ const reducers = {
compose,
contexts,
conversations,
domain_lists,
draft_statuses,
entities,
filters,