pl-fe: migrations: account aliases

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-06-26 12:39:51 +02:00
parent 6c87051af2
commit 8e98d669f7
5 changed files with 73 additions and 201 deletions

View File

@ -1,102 +0,0 @@
import { defineMessages } from 'react-intl';
import toast from 'pl-fe/toast';
import { isLoggedIn } from 'pl-fe/utils/auth';
import { getClient } from '../api';
import { importEntities } from './importer';
import type { Account as BaseAccount } from 'pl-api';
import type { Account } from 'pl-fe/normalizers/account';
import type { AppDispatch, RootState } from 'pl-fe/store';
const ALIASES_FETCH_SUCCESS = 'ALIASES_FETCH_SUCCESS' as const;
const ALIASES_SUGGESTIONS_CHANGE = 'ALIASES_SUGGESTIONS_CHANGE' as const;
const ALIASES_SUGGESTIONS_READY = 'ALIASES_SUGGESTIONS_READY' as const;
const ALIASES_SUGGESTIONS_CLEAR = 'ALIASES_SUGGESTIONS_CLEAR' as const;
const messages = defineMessages({
createSuccess: { id: 'aliases.success.add', defaultMessage: 'Account alias created successfully' },
removeSuccess: { id: 'aliases.success.remove', defaultMessage: 'Account alias removed successfully' },
});
const fetchAliases = (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
return getClient(getState).settings.getAccountAliases()
.then(response => {
dispatch(fetchAliasesSuccess(response.aliases));
});
};
const fetchAliasesSuccess = (aliases: Array<string>) => ({
type: ALIASES_FETCH_SUCCESS,
value: aliases,
});
const fetchAliasesSuggestions = (q: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
return getClient(getState()).accounts.searchAccounts(q, { resolve: true, limit: 4 })
.then((data) => {
dispatch(importEntities({ accounts: data }));
dispatch(fetchAliasesSuggestionsReady(q, data));
}).catch(error => toast.showAlertForError(error));
};
const fetchAliasesSuggestionsReady = (query: string, accounts: BaseAccount[]) => ({
type: ALIASES_SUGGESTIONS_READY,
query,
accounts,
});
const clearAliasesSuggestions = () => ({
type: ALIASES_SUGGESTIONS_CLEAR,
});
const changeAliasesSuggestions = (value: string) => ({
type: ALIASES_SUGGESTIONS_CHANGE,
value,
});
const addToAliases = (account: Account) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
return getClient(getState).settings.addAccountAlias(account.acct).then(() => {
toast.success(messages.createSuccess);
dispatch(fetchAliases);
});
};
const removeFromAliases = (account: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
return getClient(getState).settings.deleteAccountAlias(account).then(() => {
toast.success(messages.removeSuccess);
});
};
type AliasesAction =
| ReturnType<typeof fetchAliasesSuccess>
| ReturnType<typeof fetchAliasesSuggestionsReady>
| ReturnType<typeof clearAliasesSuggestions>
| ReturnType<typeof changeAliasesSuggestions>
export {
ALIASES_FETCH_SUCCESS,
ALIASES_SUGGESTIONS_CHANGE,
ALIASES_SUGGESTIONS_READY,
ALIASES_SUGGESTIONS_CLEAR,
fetchAliases,
fetchAliasesSuggestions,
clearAliasesSuggestions,
changeAliasesSuggestions,
addToAliases,
removeFromAliases,
type AliasesAction,
};

View File

@ -1,8 +1,7 @@
import clsx from 'clsx';
import React, { useEffect } from 'react';
import React, { useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { addToAliases, changeAliasesSuggestions, clearAliasesSuggestions, fetchAliases, fetchAliasesSuggestions, removeFromAliases } from 'pl-fe/actions/aliases';
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
import AccountComponent from 'pl-fe/components/account';
import Icon from 'pl-fe/components/icon';
@ -13,10 +12,10 @@ import { CardHeader, CardTitle } from 'pl-fe/components/ui/card';
import Column from 'pl-fe/components/ui/column';
import HStack from 'pl-fe/components/ui/hstack';
import Text from 'pl-fe/components/ui/text';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
import { useSearchAccounts } from 'pl-fe/queries/search/use-search';
import { useAccountAliases, useAddAccountAlias, useDeleteAccountAlias } from 'pl-fe/queries/settings/use-account-aliases';
const messages = defineMessages({
heading: { id: 'column.aliases', defaultMessage: 'Account aliases' },
@ -37,17 +36,18 @@ interface IAccount {
const Account: React.FC<IAccount> = ({ accountId, aliases }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const features = useFeatures();
const me = useAppSelector((state) => state.me);
const { account } = useAccount(accountId);
const { mutate: addAccountAlias } = useAddAccountAlias();
const apId = account?.ap_id;
const name = features.accountMoving ? account?.acct : apId;
const added = name ? aliases.includes(name) : false;
const handleOnAdd = () => dispatch(addToAliases(account!));
const handleOnAdd = () => addAccountAlias(name!);
if (!account) return null;
@ -69,28 +69,31 @@ const Account: React.FC<IAccount> = ({ accountId, aliases }) => {
);
};
const Search: React.FC = () => {
const dispatch = useAppDispatch();
interface IAliasesSearch {
onSubmit: (value: string) => void;
}
const Search: React.FC<IAliasesSearch> = ({ onSubmit }) => {
const intl = useIntl();
const value = useAppSelector(state => state.aliases.suggestions.value);
const [value, setValue] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(changeAliasesSuggestions(e.target.value));
setValue(e.target.value);
};
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.keyCode === 13) {
dispatch(fetchAliasesSuggestions(value));
onSubmit(value);
}
};
const handleSubmit = () => {
dispatch(fetchAliasesSuggestions(value));
onSubmit(value);
};
const handleClear = () => {
dispatch(clearAliasesSuggestions());
onSubmit('');
};
const hasValue = value.length > 0;
@ -120,27 +123,15 @@ const Search: React.FC = () => {
const AliasesPage = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const features = useFeatures();
const { account } = useOwnAccount();
const aliases = useAppSelector((state): Array<string> => {
if (features.accountMoving) {
return [...state.aliases.aliases.items];
} else {
return account?.__meta.pleroma?.also_known_as ?? [];
}
});
const [query, setQuery] = useState('');
const searchAccountIds = useAppSelector((state) => state.aliases.suggestions.items);
const loaded = useAppSelector((state) => state.aliases.suggestions.loaded);
const { data: aliases = [] } = useAccountAliases();
const { data: searchAccountIds = [], isFetched } = useSearchAccounts(query);
const { mutate: deleteAccountAlias } = useDeleteAccountAlias();
useEffect(() => {
dispatch(fetchAliases);
}, []);
const handleFilterDelete: React.MouseEventHandler<HTMLDivElement> = e => {
dispatch(removeFromAliases(e.currentTarget.dataset.value as string));
const handleAliasDelete: React.MouseEventHandler<HTMLDivElement> = e => {
deleteAccountAlias(e.currentTarget.dataset.value as string);
};
const emptyMessage = <FormattedMessage id='empty_column.aliases' defaultMessage="You haven't created any account alias yet." />;
@ -150,14 +141,14 @@ const AliasesPage = () => {
<CardHeader>
<CardTitle title={intl.formatMessage(messages.subheading_add_new)} />
</CardHeader>
<Search />
<Search onSubmit={setQuery} />
{
loaded && searchAccountIds.length === 0 ? (
isFetched && searchAccountIds.length === 0 ? (
<div className='empty-column-indicator'>
<FormattedMessage id='empty_column.aliases.suggestions' defaultMessage='There are no account suggestions available for the provided term.' />
</div>
) : (
<div className='mb-4 overflow-y-auto'>
<div className='mb-4 max-h-72 overflow-y-auto'>
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} aliases={aliases} />)}
</div>
)
@ -177,7 +168,7 @@ const AliasesPage = () => {
{' '}
<Text tag='span'>{alias}</Text>
</div>
<div className='flex items-center' role='button' tabIndex={0} onClick={handleFilterDelete} data-value={alias} aria-label={intl.formatMessage(messages.delete)}>
<div className='flex items-center' role='button' tabIndex={0} onClick={handleAliasDelete} data-value={alias} aria-label={intl.formatMessage(messages.delete)}>
<Icon className='mr-1.5' src={require('@tabler/icons/outline/x.svg')} />
<Text weight='bold' theme='muted'><FormattedMessage id='aliases.aliases_list_delete' defaultMessage='Unlink alias' /></Text>
</div>

View File

@ -0,0 +1,47 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
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';
const useAccountAliases = () => {
const client = useClient();
const features = useFeatures();
const { account } = useOwnAccount();
return useQuery({
queryKey: ['settings', 'accountAliases'],
queryFn: async (): Promise<Array<string>> => {
if (features.accountMoving) return (await client.settings.getAccountAliases()).aliases;
return account?.__meta.pleroma?.also_known_as ?? [];
},
});
};
const useAddAccountAlias = () => {
const client = useClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['settings', 'accountAliases'],
mutationFn: (acct: string) => client.settings.addAccountAlias(acct),
onSettled: () => queryClient.invalidateQueries({
queryKey: ['settings', 'accountAliases'],
}),
});
};
const useDeleteAccountAlias = () => {
const client = useClient();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['settings', 'accountAliases'],
mutationFn: (acct: string) => client.settings.deleteAccountAlias(acct),
onSettled: () => queryClient.invalidateQueries({
queryKey: ['settings', 'accountAliases'],
}),
});
};
export { useAccountAliases, useAddAccountAlias, useDeleteAccountAlias };

View File

@ -1,62 +0,0 @@
import { create } from 'mutative';
import {
ALIASES_SUGGESTIONS_READY,
ALIASES_SUGGESTIONS_CLEAR,
ALIASES_SUGGESTIONS_CHANGE,
ALIASES_FETCH_SUCCESS,
AliasesAction,
} from '../actions/aliases';
interface State {
aliases: {
items: Array<string>;
loaded: boolean;
};
suggestions: {
items: Array<string>;
value: string;
loaded: boolean;
};
}
const initialState: State = {
aliases: {
items: [],
loaded: false,
},
suggestions: {
items: [],
value: '',
loaded: false,
},
};
const aliasesReducer = (state = initialState, action: AliasesAction): State => {
switch (action.type) {
case ALIASES_FETCH_SUCCESS:
return create(state, (draft) => {
draft.aliases.items = action.value;
});
case ALIASES_SUGGESTIONS_CHANGE:
return create(state, (draft) => {
draft.suggestions.value = action.value;
draft.suggestions.loaded = false;
});
case ALIASES_SUGGESTIONS_READY:
return create(state, (draft) => {
draft.suggestions.items = action.accounts.map((item) => item.id);
draft.suggestions.loaded = true;
});
case ALIASES_SUGGESTIONS_CLEAR:
return create(state, (draft) => {
draft.suggestions.items = [];
draft.suggestions.value = '';
draft.suggestions.loaded = false;
});
default:
return state;
}
};
export { aliasesReducer as default };

View File

@ -7,7 +7,6 @@ import entities from 'pl-fe/entity-store/reducer';
import accounts_meta from './accounts-meta';
import admin from './admin';
import admin_user_index from './admin-user-index';
import aliases from './aliases';
import auth from './auth';
import compose from './compose';
import contexts from './contexts';
@ -34,7 +33,6 @@ const reducers = {
accounts_meta,
admin,
admin_user_index,
aliases,
auth,
compose,
contexts,