kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'req-identity' into 'main'
Nostr identity request See merge request soapbox-pub/soapbox!3061environments/review-main-yi2y9f/deployments/4692
commit
a09327abeb
|
@ -39,6 +39,10 @@ const ADMIN_USERS_APPROVE_REQUEST = 'ADMIN_USERS_APPROVE_REQUEST';
|
||||||
const ADMIN_USERS_APPROVE_SUCCESS = 'ADMIN_USERS_APPROVE_SUCCESS';
|
const ADMIN_USERS_APPROVE_SUCCESS = 'ADMIN_USERS_APPROVE_SUCCESS';
|
||||||
const ADMIN_USERS_APPROVE_FAIL = 'ADMIN_USERS_APPROVE_FAIL';
|
const ADMIN_USERS_APPROVE_FAIL = 'ADMIN_USERS_APPROVE_FAIL';
|
||||||
|
|
||||||
|
const ADMIN_USERS_REJECT_REQUEST = 'ADMIN_USERS_REJECT_REQUEST';
|
||||||
|
const ADMIN_USERS_REJECT_SUCCESS = 'ADMIN_USERS_REJECT_SUCCESS';
|
||||||
|
const ADMIN_USERS_REJECT_FAIL = 'ADMIN_USERS_REJECT_FAIL';
|
||||||
|
|
||||||
const ADMIN_USERS_DEACTIVATE_REQUEST = 'ADMIN_USERS_DEACTIVATE_REQUEST';
|
const ADMIN_USERS_DEACTIVATE_REQUEST = 'ADMIN_USERS_DEACTIVATE_REQUEST';
|
||||||
const ADMIN_USERS_DEACTIVATE_SUCCESS = 'ADMIN_USERS_DEACTIVATE_SUCCESS';
|
const ADMIN_USERS_DEACTIVATE_SUCCESS = 'ADMIN_USERS_DEACTIVATE_SUCCESS';
|
||||||
const ADMIN_USERS_DEACTIVATE_FAIL = 'ADMIN_USERS_DEACTIVATE_FAIL';
|
const ADMIN_USERS_DEACTIVATE_FAIL = 'ADMIN_USERS_DEACTIVATE_FAIL';
|
||||||
|
@ -266,6 +270,14 @@ const fetchUsers = (filters: string[] = [], page = 1, query?: string | null, pag
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const revokeName = (accountId: string, reportId?: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||||
|
api(getState)
|
||||||
|
.post(`/api/v1/admin/accounts/${accountId}/action`, {
|
||||||
|
type: 'revoke_name',
|
||||||
|
report_id: reportId,
|
||||||
|
});
|
||||||
|
|
||||||
const deactivateMastodonUsers = (accountIds: string[], reportId?: string) =>
|
const deactivateMastodonUsers = (accountIds: string[], reportId?: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||||
Promise.all(accountIds.map(accountId => {
|
Promise.all(accountIds.map(accountId => {
|
||||||
|
@ -309,56 +321,80 @@ const deactivateUsers = (accountIds: string[], reportId?: string) =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteUsers = (accountIds: string[]) =>
|
const deleteUser = (accountId: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
const nicknames = accountIdsToAccts(getState(), [accountId]);
|
||||||
dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountIds });
|
dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountId });
|
||||||
return api(getState)
|
return api(getState)
|
||||||
.delete('/api/v1/pleroma/admin/users', { data: { nicknames } })
|
.delete('/api/v1/pleroma/admin/users', { data: { nicknames } })
|
||||||
.then(({ data: nicknames }) => {
|
.then(({ data: nicknames }) => {
|
||||||
dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames, accountIds });
|
dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames, accountId });
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, accountIds });
|
dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, accountId });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveMastodonUsers = (accountIds: string[]) =>
|
const approveMastodonUser = (accountId: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||||
Promise.all(accountIds.map(accountId => {
|
api(getState)
|
||||||
api(getState)
|
.post(`/api/v1/admin/accounts/${accountId}/approve`)
|
||||||
.post(`/api/v1/admin/accounts/${accountId}/approve`)
|
.then(({ data: user }) => {
|
||||||
.then(({ data: user }) => {
|
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user, accountId });
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users: [user], accountIds: [accountId] });
|
}).catch(error => {
|
||||||
}).catch(error => {
|
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId });
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds: [accountId] });
|
});
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
const approvePleromaUsers = (accountIds: string[]) =>
|
const approvePleromaUser = (accountId: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
const nicknames = accountIdsToAccts(getState(), [accountId]);
|
||||||
return api(getState)
|
return api(getState)
|
||||||
.patch('/api/v1/pleroma/admin/users/approve', { nicknames })
|
.patch('/api/v1/pleroma/admin/users/approve', { nicknames })
|
||||||
.then(({ data: { users } }) => {
|
.then(({ data: { users } }) => {
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users, accountIds });
|
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user: users[0], accountId });
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds });
|
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveUsers = (accountIds: string[]) =>
|
const rejectMastodonUser = (accountId: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||||
|
api(getState)
|
||||||
|
.post(`/api/v1/admin/accounts/${accountId}/reject`)
|
||||||
|
.then(({ data: user }) => {
|
||||||
|
dispatch({ type: ADMIN_USERS_REJECT_SUCCESS, user, accountId });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_REJECT_FAIL, error, accountId });
|
||||||
|
});
|
||||||
|
|
||||||
|
const approveUser = (accountId: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
const instance = state.instance;
|
const instance = state.instance;
|
||||||
const features = getFeatures(instance);
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountIds });
|
dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountId });
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
if (features.mastodonAdmin) {
|
||||||
return dispatch(approveMastodonUsers(accountIds));
|
return dispatch(approveMastodonUser(accountId));
|
||||||
} else {
|
} else {
|
||||||
return dispatch(approvePleromaUsers(accountIds));
|
return dispatch(approvePleromaUser(accountId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectUser = (accountId: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const instance = state.instance;
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
dispatch({ type: ADMIN_USERS_REJECT_REQUEST, accountId });
|
||||||
|
|
||||||
|
if (features.mastodonAdmin) {
|
||||||
|
return dispatch(rejectMastodonUser(accountId));
|
||||||
|
} else {
|
||||||
|
return dispatch(deleteUser(accountId));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -562,6 +598,9 @@ export {
|
||||||
ADMIN_USERS_APPROVE_REQUEST,
|
ADMIN_USERS_APPROVE_REQUEST,
|
||||||
ADMIN_USERS_APPROVE_SUCCESS,
|
ADMIN_USERS_APPROVE_SUCCESS,
|
||||||
ADMIN_USERS_APPROVE_FAIL,
|
ADMIN_USERS_APPROVE_FAIL,
|
||||||
|
ADMIN_USERS_REJECT_REQUEST,
|
||||||
|
ADMIN_USERS_REJECT_SUCCESS,
|
||||||
|
ADMIN_USERS_REJECT_FAIL,
|
||||||
ADMIN_USERS_DEACTIVATE_REQUEST,
|
ADMIN_USERS_DEACTIVATE_REQUEST,
|
||||||
ADMIN_USERS_DEACTIVATE_SUCCESS,
|
ADMIN_USERS_DEACTIVATE_SUCCESS,
|
||||||
ADMIN_USERS_DEACTIVATE_FAIL,
|
ADMIN_USERS_DEACTIVATE_FAIL,
|
||||||
|
@ -597,8 +636,10 @@ export {
|
||||||
closeReports,
|
closeReports,
|
||||||
fetchUsers,
|
fetchUsers,
|
||||||
deactivateUsers,
|
deactivateUsers,
|
||||||
deleteUsers,
|
deleteUser,
|
||||||
approveUsers,
|
approveUser,
|
||||||
|
rejectUser,
|
||||||
|
revokeName,
|
||||||
deleteStatus,
|
deleteStatus,
|
||||||
toggleStatusSensitivity,
|
toggleStatusSensitivity,
|
||||||
tagUsers,
|
tagUsers,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { defineMessages, IntlShape } from 'react-intl';
|
import { defineMessages, IntlShape } from 'react-intl';
|
||||||
|
|
||||||
import { fetchAccountByUsername } from 'soapbox/actions/accounts';
|
import { fetchAccountByUsername } from 'soapbox/actions/accounts';
|
||||||
import { deactivateUsers, deleteUsers, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin';
|
import { deactivateUsers, deleteUser, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin';
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import OutlineBox from 'soapbox/components/outline-box';
|
import OutlineBox from 'soapbox/components/outline-box';
|
||||||
import { Stack, Text } from 'soapbox/components/ui';
|
import { Stack, Text } from 'soapbox/components/ui';
|
||||||
|
@ -102,7 +102,7 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
|
||||||
confirm,
|
confirm,
|
||||||
checkbox,
|
checkbox,
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
dispatch(deleteUsers([accountId])).then(() => {
|
dispatch(deleteUser(accountId)).then(() => {
|
||||||
const message = intl.formatMessage(messages.userDeleted, { acct });
|
const message = intl.formatMessage(messages.userDeleted, { acct });
|
||||||
dispatch(fetchAccountByUsername(acct));
|
dispatch(fetchAccountByUsername(acct));
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
|
|
|
@ -71,6 +71,7 @@ const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IAccount {
|
export interface IAccount {
|
||||||
|
acct?: string;
|
||||||
account: AccountSchema;
|
account: AccountSchema;
|
||||||
action?: React.ReactElement;
|
action?: React.ReactElement;
|
||||||
actionAlignment?: 'center' | 'top';
|
actionAlignment?: 'center' | 'top';
|
||||||
|
@ -99,6 +100,7 @@ export interface IAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Account = ({
|
const Account = ({
|
||||||
|
acct,
|
||||||
account,
|
account,
|
||||||
actionType,
|
actionType,
|
||||||
action,
|
action,
|
||||||
|
@ -228,7 +230,7 @@ const Account = ({
|
||||||
|
|
||||||
<Stack space={withAccountNote || note ? 1 : 0}>
|
<Stack space={withAccountNote || note ? 1 : 0}>
|
||||||
<HStack alignItems='center' space={1}>
|
<HStack alignItems='center' space={1}>
|
||||||
<Text theme='muted' size='sm' direction='ltr' truncate>@{username}</Text>
|
<Text theme='muted' size='sm' direction='ltr' truncate>@{acct ?? username}</Text>
|
||||||
|
|
||||||
{account.pleroma?.favicon && (
|
{account.pleroma?.favicon && (
|
||||||
<InstanceFavicon account={account} disabled={!withLinkToProfile} />
|
<InstanceFavicon account={account} disabled={!withLinkToProfile} />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { approveUsers, deleteUsers } from 'soapbox/actions/admin';
|
import { approveUser, rejectUser } from 'soapbox/actions/admin';
|
||||||
import { useAccount } from 'soapbox/api/hooks';
|
import { useAccount } from 'soapbox/api/hooks';
|
||||||
import Account from 'soapbox/components/account';
|
import Account from 'soapbox/components/account';
|
||||||
import { AuthorizeRejectButtons } from 'soapbox/components/authorize-reject-buttons';
|
import { AuthorizeRejectButtons } from 'soapbox/components/authorize-reject-buttons';
|
||||||
|
@ -14,18 +14,19 @@ interface IUnapprovedAccount {
|
||||||
const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
|
const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { account } = useAccount(accountId);
|
|
||||||
const adminAccount = useAppSelector(state => state.admin.users.get(accountId));
|
const adminAccount = useAppSelector(state => state.admin.users.get(accountId));
|
||||||
|
const { account } = useAccount(adminAccount?.account || undefined);
|
||||||
|
|
||||||
if (!account) return null;
|
if (!adminAccount || !account) return null;
|
||||||
|
|
||||||
const handleApprove = () => dispatch(approveUsers([account.id]));
|
const handleApprove = () => dispatch(approveUser(adminAccount.id));
|
||||||
const handleReject = () => dispatch(deleteUsers([account.id]));
|
const handleReject = () => dispatch(rejectUser(adminAccount.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Account
|
<Account
|
||||||
key={account.id}
|
key={adminAccount.id}
|
||||||
account={account}
|
account={account}
|
||||||
|
acct={`${adminAccount.username}@${adminAccount.domain}`}
|
||||||
note={adminAccount?.invite_request || ''}
|
note={adminAccount?.invite_request || ''}
|
||||||
action={(
|
action={(
|
||||||
<AuthorizeRejectButtons
|
<AuthorizeRejectButtons
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { patchMe } from 'soapbox/actions/me';
|
import { patchMe } from 'soapbox/actions/me';
|
||||||
import List, { ListItem } from 'soapbox/components/list';
|
import List, { ListItem } from 'soapbox/components/list';
|
||||||
import { Button, Column, Emoji, HStack, Icon, Input, Tooltip } from 'soapbox/components/ui';
|
import { Button, CardHeader, CardTitle, Column, Emoji, Form, HStack, Icon, Input, Textarea, Tooltip } from 'soapbox/components/ui';
|
||||||
import { useNostr } from 'soapbox/contexts/nostr-context';
|
import { useApi, useAppDispatch, useInstance, useOwnAccount } from 'soapbox/hooks';
|
||||||
import { useNostrReq } from 'soapbox/features/nostr/hooks/useNostrReq';
|
import { adminAccountSchema } from 'soapbox/schemas/admin-account';
|
||||||
import { useAppDispatch, useInstance, useOwnAccount } from 'soapbox/hooks';
|
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
|
|
||||||
interface IEditIdentity {
|
interface IEditIdentity {
|
||||||
|
@ -18,6 +18,7 @@ const messages = defineMessages({
|
||||||
unverified: { id: 'edit_profile.fields.nip05_unverified', defaultMessage: 'Name could not be verified and won\'t be used.' },
|
unverified: { id: 'edit_profile.fields.nip05_unverified', defaultMessage: 'Name could not be verified and won\'t be used.' },
|
||||||
success: { id: 'edit_profile.success', defaultMessage: 'Your profile has been successfully saved!' },
|
success: { id: 'edit_profile.success', defaultMessage: 'Your profile has been successfully saved!' },
|
||||||
error: { id: 'edit_profile.error', defaultMessage: 'Profile update failed' },
|
error: { id: 'edit_profile.error', defaultMessage: 'Profile update failed' },
|
||||||
|
placeholder: { id: 'edit_identity.reason_placeholder', defaultMessage: 'Why do you want this name?' },
|
||||||
});
|
});
|
||||||
|
|
||||||
/** EditIdentity component. */
|
/** EditIdentity component. */
|
||||||
|
@ -26,77 +27,111 @@ const EditIdentity: React.FC<IEditIdentity> = () => {
|
||||||
const instance = useInstance();
|
const instance = useInstance();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
const { relay, signer } = useNostr();
|
const { mutate } = useRequestName();
|
||||||
|
|
||||||
|
const { data: approvedNames } = useNames();
|
||||||
|
const { data: pendingNames } = usePendingNames();
|
||||||
|
|
||||||
const admin = instance.nostr?.pubkey;
|
|
||||||
const pubkey = account?.nostr?.pubkey;
|
|
||||||
const [username, setUsername] = useState<string>('');
|
const [username, setUsername] = useState<string>('');
|
||||||
|
const [reason, setReason] = useState<string>('');
|
||||||
const { events: labels } = useNostrReq(
|
|
||||||
(admin && pubkey)
|
|
||||||
? [{ kinds: [1985], authors: [admin], '#L': ['nip05'], '#p': [pubkey] }]
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
const updateNip05 = async (nip05: string): Promise<void> => {
|
const updateName = async (name: string): Promise<void> => {
|
||||||
if (account.source?.nostr?.nip05 === nip05) return;
|
if (account.source?.nostr?.nip05 === name) return;
|
||||||
try {
|
try {
|
||||||
await dispatch(patchMe({ nip05 }));
|
await dispatch(patchMe({ nip05: name }));
|
||||||
toast.success(intl.formatMessage(messages.success));
|
toast.success(intl.formatMessage(messages.success));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(intl.formatMessage(messages.error));
|
toast.error(intl.formatMessage(messages.error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = () => {
|
||||||
if (!admin || !signer || !relay) return;
|
const name = `${username}@${instance.domain}`;
|
||||||
|
mutate({ name, reason });
|
||||||
const event = await signer.signEvent({
|
|
||||||
kind: 5950,
|
|
||||||
content: '',
|
|
||||||
tags: [
|
|
||||||
['i', `${username}@${instance.domain}`, 'text'],
|
|
||||||
['p', admin],
|
|
||||||
],
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
});
|
|
||||||
|
|
||||||
await relay.event(event);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)}>
|
<Column label={intl.formatMessage(messages.title)}>
|
||||||
<List>
|
<div className='space-y-4'>
|
||||||
{labels.map((label) => {
|
<Form>
|
||||||
const identifier = label.tags.find(([name]) => name === 'l')?.[1];
|
<UsernameInput value={username} onChange={(e) => setUsername(e.target.value)} />
|
||||||
if (!identifier) return null;
|
<Textarea
|
||||||
|
name='reason'
|
||||||
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
|
maxLength={500}
|
||||||
|
onChange={(e) => setReason(e.target.value)}
|
||||||
|
value={reason}
|
||||||
|
autoGrow
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button theme='accent' onClick={submit}>
|
||||||
|
<FormattedMessage id='edit_identity.request' defaultMessage='Request' />
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
|
||||||
return (
|
{((approvedNames?.length ?? 0) > 0) && (
|
||||||
<ListItem
|
<>
|
||||||
key={identifier}
|
<CardHeader>
|
||||||
label={
|
<CardTitle title={<FormattedMessage id='edit_identity.names_title' defaultMessage='Names' />} />
|
||||||
<HStack alignItems='center' space={2}>
|
</CardHeader>
|
||||||
<span>{identifier}</span>
|
|
||||||
{(account.source?.nostr?.nip05 === identifier && account.acct !== identifier) && (
|
<List>
|
||||||
<Tooltip text={intl.formatMessage(messages.unverified)}>
|
{approvedNames?.map(({ username, domain }) => {
|
||||||
<div>
|
const identifier = `${username}@${domain}`;
|
||||||
<Emoji className='h-4 w-4' emoji='⚠️' />
|
if (!identifier) return null;
|
||||||
</div>
|
|
||||||
</Tooltip>
|
return (
|
||||||
)}
|
<ListItem
|
||||||
</HStack>
|
key={identifier}
|
||||||
}
|
label={
|
||||||
isSelected={account.source?.nostr?.nip05 === identifier}
|
<HStack alignItems='center' space={2}>
|
||||||
onSelect={() => updateNip05(identifier)}
|
<span>{identifier}</span>
|
||||||
/>
|
{(account.source?.nostr?.nip05 === identifier && account.acct !== identifier) && (
|
||||||
);
|
<Tooltip text={intl.formatMessage(messages.unverified)}>
|
||||||
})}
|
<div>
|
||||||
<ListItem label={<UsernameInput value={username} onChange={(e) => setUsername(e.target.value)} />}>
|
<Emoji className='h-4 w-4' emoji='⚠️' />
|
||||||
<Button theme='accent' onClick={submit}>Add</Button>
|
</div>
|
||||||
</ListItem>
|
</Tooltip>
|
||||||
</List>
|
)}
|
||||||
|
</HStack>
|
||||||
|
}
|
||||||
|
isSelected={account.source?.nostr?.nip05 === identifier}
|
||||||
|
onSelect={() => updateName(identifier)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{((pendingNames?.length ?? 0) > 0) && (
|
||||||
|
<>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle title={<FormattedMessage id='edit_identity.pending_names_title' defaultMessage='Requested Names' />} />
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<List>
|
||||||
|
{pendingNames?.map(({ username, domain }) => {
|
||||||
|
const identifier = `${username}@${domain}`;
|
||||||
|
if (!identifier) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={identifier}
|
||||||
|
label={
|
||||||
|
<HStack alignItems='center' space={2}>
|
||||||
|
<span>{identifier}</span>
|
||||||
|
</HStack>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -119,4 +154,43 @@ const UsernameInput: React.FC<React.ComponentProps<typeof Input>> = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface NameRequestData {
|
||||||
|
name: string;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useRequestName() {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: NameRequestData) => api.post('/api/v1/ditto/names', data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function useNames() {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['names', 'approved'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await api.get('/api/v1/ditto/names?approved=true');
|
||||||
|
return adminAccountSchema.array().parse(data);
|
||||||
|
},
|
||||||
|
placeholderData: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePendingNames() {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['names', 'pending'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await api.get('/api/v1/ditto/names?approved=false');
|
||||||
|
return adminAccountSchema.array().parse(data);
|
||||||
|
},
|
||||||
|
placeholderData: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default EditIdentity;
|
export default EditIdentity;
|
|
@ -129,8 +129,8 @@ interface AccountCredentials {
|
||||||
birthday?: string;
|
birthday?: string;
|
||||||
/** Nostr NIP-05 identifier. */
|
/** Nostr NIP-05 identifier. */
|
||||||
nip05?: string;
|
nip05?: string;
|
||||||
/**
|
/**
|
||||||
* Lightning address.
|
* Lightning address.
|
||||||
* https://github.com/lnurl/luds/blob/luds/16.md
|
* https://github.com/lnurl/luds/blob/luds/16.md
|
||||||
*/
|
*/
|
||||||
lud16?: string;
|
lud16?: string;
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { defineMessages, useIntl, FormattedMessage, IntlShape, MessageDescriptor, defineMessage } from 'react-intl';
|
import { defineMessages, useIntl, IntlShape, MessageDescriptor, defineMessage, FormattedMessage } from 'react-intl';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { mentionCompose } from 'soapbox/actions/compose';
|
import { mentionCompose } from 'soapbox/actions/compose';
|
||||||
import { reblog, favourite, unreblog, unfavourite } from 'soapbox/actions/interactions';
|
import { reblog, favourite, unreblog, unfavourite } from 'soapbox/actions/interactions';
|
||||||
|
import { patchMe } from 'soapbox/actions/me';
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
import { getSettings } from 'soapbox/actions/settings';
|
||||||
import { hideStatus, revealStatus } from 'soapbox/actions/statuses';
|
import { hideStatus, revealStatus } from 'soapbox/actions/statuses';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import { HStack, Text, Emoji } from 'soapbox/components/ui';
|
import { HStack, Text, Emoji, Button, Stack } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
import AccountContainer from 'soapbox/containers/account-container';
|
||||||
import StatusContainer from 'soapbox/containers/status-container';
|
import StatusContainer from 'soapbox/containers/status-container';
|
||||||
import { HotKeys } from 'soapbox/features/ui/components/hotkeys';
|
import { HotKeys } from 'soapbox/features/ui/components/hotkeys';
|
||||||
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
|
||||||
import { makeGetNotification } from 'soapbox/selectors';
|
import { makeGetNotification } from 'soapbox/selectors';
|
||||||
|
import toast from 'soapbox/toast';
|
||||||
import { NotificationType, validType } from 'soapbox/utils/notification';
|
import { NotificationType, validType } from 'soapbox/utils/notification';
|
||||||
|
|
||||||
import type { ScrollPosition } from 'soapbox/components/status';
|
import type { ScrollPosition } from 'soapbox/components/status';
|
||||||
|
@ -56,6 +58,7 @@ const icons: Record<NotificationType, string> = {
|
||||||
'pleroma:event_reminder': require('@tabler/icons/outline/calendar-time.svg'),
|
'pleroma:event_reminder': require('@tabler/icons/outline/calendar-time.svg'),
|
||||||
'pleroma:participation_request': require('@tabler/icons/outline/calendar-event.svg'),
|
'pleroma:participation_request': require('@tabler/icons/outline/calendar-event.svg'),
|
||||||
'pleroma:participation_accepted': require('@tabler/icons/outline/calendar-event.svg'),
|
'pleroma:participation_accepted': require('@tabler/icons/outline/calendar-event.svg'),
|
||||||
|
'ditto:name_grant': require('@tabler/icons/outline/user-check.svg'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const nameMessage = defineMessage({
|
const nameMessage = defineMessage({
|
||||||
|
@ -63,7 +66,7 @@ const nameMessage = defineMessage({
|
||||||
defaultMessage: '{link}{others}',
|
defaultMessage: '{link}{others}',
|
||||||
});
|
});
|
||||||
|
|
||||||
const messages: Record<NotificationType, MessageDescriptor> = defineMessages({
|
const notificationMessages: Record<NotificationType, MessageDescriptor> = defineMessages({
|
||||||
follow: {
|
follow: {
|
||||||
id: 'notification.follow',
|
id: 'notification.follow',
|
||||||
defaultMessage: '{name} followed you',
|
defaultMessage: '{name} followed you',
|
||||||
|
@ -132,29 +135,32 @@ const messages: Record<NotificationType, MessageDescriptor> = defineMessages({
|
||||||
id: 'notification.pleroma:participation_accepted',
|
id: 'notification.pleroma:participation_accepted',
|
||||||
defaultMessage: 'You were accepted to join the event',
|
defaultMessage: 'You were accepted to join the event',
|
||||||
},
|
},
|
||||||
|
'ditto:name_grant': {
|
||||||
|
id: 'notification.ditto:name_grant',
|
||||||
|
defaultMessage: 'You were granted the name {acct}',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
updateNameSuccess: { id: 'notification.update_name_success', defaultMessage: 'Name updated successfully' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildMessage = (
|
const buildMessage = (
|
||||||
intl: IntlShape,
|
intl: IntlShape,
|
||||||
type: NotificationType,
|
type: NotificationType,
|
||||||
account: AccountEntity,
|
account: AccountEntity,
|
||||||
totalCount: number | null,
|
acct: string | undefined,
|
||||||
targetName: string,
|
targetName: string,
|
||||||
instanceTitle: string,
|
instanceTitle: string,
|
||||||
): React.ReactNode => {
|
): React.ReactNode => {
|
||||||
const link = buildLink(account);
|
const link = buildLink(account);
|
||||||
const name = intl.formatMessage(nameMessage, {
|
const name = intl.formatMessage(nameMessage, {
|
||||||
link,
|
link,
|
||||||
others: totalCount && totalCount > 0 ? (
|
others: '',
|
||||||
<FormattedMessage
|
|
||||||
id='notification.others'
|
|
||||||
defaultMessage='+ {count, plural, one {# other} other {# others}}'
|
|
||||||
values={{ count: totalCount - 1 }}
|
|
||||||
/>
|
|
||||||
) : '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return intl.formatMessage(messages[type], {
|
return intl.formatMessage(notificationMessages[type], {
|
||||||
|
acct,
|
||||||
name,
|
name,
|
||||||
targetName,
|
targetName,
|
||||||
instance: instanceTitle,
|
instance: instanceTitle,
|
||||||
|
@ -274,6 +280,11 @@ const Notification: React.FC<INotification> = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateName = async (name: string) => {
|
||||||
|
await dispatch(patchMe({ nip05: name }));
|
||||||
|
toast.success(messages.updateNameSuccess);
|
||||||
|
};
|
||||||
|
|
||||||
const renderIcon = (): React.ReactNode => {
|
const renderIcon = (): React.ReactNode => {
|
||||||
if (type === 'pleroma:emoji_reaction' && notification.emoji) {
|
if (type === 'pleroma:emoji_reaction' && notification.emoji) {
|
||||||
return (
|
return (
|
||||||
|
@ -349,19 +360,32 @@ const Notification: React.FC<INotification> = (props) => {
|
||||||
showGroup={false}
|
showGroup={false}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
case 'ditto:name_grant':
|
||||||
|
return (
|
||||||
|
<Stack className='p-4'>
|
||||||
|
<Button onClick={() => updateName(notification.name)}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.set_name' defaultMessage='Set name to {name}'
|
||||||
|
values={{ name: notification.name }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const acct = notification.name;
|
||||||
const targetName = notification.target && typeof notification.target === 'object' ? notification.target.acct : '';
|
const targetName = notification.target && typeof notification.target === 'object' ? notification.target.acct : '';
|
||||||
|
|
||||||
const message: React.ReactNode = validType(type) && account && typeof account === 'object' ? buildMessage(intl, type, account, notification.total_count, targetName, instance.title) : null;
|
const message: React.ReactNode = validType(type) && account && typeof account === 'object' ? buildMessage(intl, type, account, acct, targetName, instance.title) : null;
|
||||||
|
|
||||||
const ariaLabel = validType(type) ? (
|
const ariaLabel = validType(type) ? (
|
||||||
notificationForScreenReader(
|
notificationForScreenReader(
|
||||||
intl,
|
intl,
|
||||||
intl.formatMessage(messages[type], {
|
intl.formatMessage(notificationMessages[type], {
|
||||||
|
acct,
|
||||||
name: account && typeof account === 'object' ? account.acct : '',
|
name: account && typeof account === 'object' ? account.acct : '',
|
||||||
targetName,
|
targetName,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { ChangeEventHandler, useState } from 'react';
|
import React, { ChangeEventHandler, useState } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { setBadges as saveBadges } from 'soapbox/actions/admin';
|
import { revokeName, setBadges as saveBadges } from 'soapbox/actions/admin';
|
||||||
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
||||||
import { useAccount } from 'soapbox/api/hooks';
|
import { useAccount } from 'soapbox/api/hooks';
|
||||||
import { useSuggest, useVerify } from 'soapbox/api/hooks/admin';
|
import { useSuggest, useVerify } from 'soapbox/api/hooks/admin';
|
||||||
|
@ -25,6 +25,7 @@ const messages = defineMessages({
|
||||||
userSuggested: { id: 'admin.users.user_suggested_message', defaultMessage: '@{acct} was suggested' },
|
userSuggested: { id: 'admin.users.user_suggested_message', defaultMessage: '@{acct} was suggested' },
|
||||||
userUnsuggested: { id: 'admin.users.user_unsuggested_message', defaultMessage: '@{acct} was unsuggested' },
|
userUnsuggested: { id: 'admin.users.user_unsuggested_message', defaultMessage: '@{acct} was unsuggested' },
|
||||||
badgesSaved: { id: 'admin.users.badges_saved_message', defaultMessage: 'Custom badges updated.' },
|
badgesSaved: { id: 'admin.users.badges_saved_message', defaultMessage: 'Custom badges updated.' },
|
||||||
|
revokedName: { id: 'admin.users.revoked_name_message', defaultMessage: 'Name revoked.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IAccountModerationModal {
|
interface IAccountModerationModal {
|
||||||
|
@ -88,6 +89,12 @@ const AccountModerationModal: React.FC<IAccountModerationModal> = ({ onClose, ac
|
||||||
dispatch(deactivateUserModal(intl, account.id));
|
dispatch(deactivateUserModal(intl, account.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRevokeName = () => {
|
||||||
|
dispatch(revokeName(account.id))
|
||||||
|
.then(() => toast.success(intl.formatMessage(messages.revokedName)))
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
dispatch(deleteUserModal(intl, account.id));
|
dispatch(deleteUserModal(intl, account.id));
|
||||||
};
|
};
|
||||||
|
@ -151,6 +158,13 @@ const AccountModerationModal: React.FC<IAccountModerationModal> = ({ onClose, ac
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
|
{features.revokeName && (
|
||||||
|
<ListItem
|
||||||
|
label={<FormattedMessage id='account_moderation_modal.fields.revoke_name' defaultMessage='Revoke name' />}
|
||||||
|
onClick={handleRevokeName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
label={<FormattedMessage id='account_moderation_modal.fields.deactivate' defaultMessage='Deactivate account' />}
|
label={<FormattedMessage id='account_moderation_modal.fields.deactivate' defaultMessage='Deactivate account' />}
|
||||||
onClick={handleDeactivate}
|
onClick={handleDeactivate}
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
"account_moderation_modal.fields.badges": "Custom badges",
|
"account_moderation_modal.fields.badges": "Custom badges",
|
||||||
"account_moderation_modal.fields.deactivate": "Deactivate account",
|
"account_moderation_modal.fields.deactivate": "Deactivate account",
|
||||||
"account_moderation_modal.fields.delete": "Delete account",
|
"account_moderation_modal.fields.delete": "Delete account",
|
||||||
|
"account_moderation_modal.fields.revoke_name": "Revoke name",
|
||||||
"account_moderation_modal.fields.suggested": "Suggested in people to follow",
|
"account_moderation_modal.fields.suggested": "Suggested in people to follow",
|
||||||
"account_moderation_modal.fields.verified": "Verified account",
|
"account_moderation_modal.fields.verified": "Verified account",
|
||||||
"account_moderation_modal.info.id": "ID: {id}",
|
"account_moderation_modal.info.id": "ID: {id}",
|
||||||
|
@ -183,6 +184,7 @@
|
||||||
"admin.users.actions.promote_to_moderator_message": "@{acct} was promoted to a moderator",
|
"admin.users.actions.promote_to_moderator_message": "@{acct} was promoted to a moderator",
|
||||||
"admin.users.badges_saved_message": "Custom badges updated.",
|
"admin.users.badges_saved_message": "Custom badges updated.",
|
||||||
"admin.users.remove_donor_message": "@{acct} was removed as a donor",
|
"admin.users.remove_donor_message": "@{acct} was removed as a donor",
|
||||||
|
"admin.users.revoked_name_message": "Name revoked.",
|
||||||
"admin.users.set_donor_message": "@{acct} was set as a donor",
|
"admin.users.set_donor_message": "@{acct} was set as a donor",
|
||||||
"admin.users.user_deactivated_message": "@{acct} was deactivated",
|
"admin.users.user_deactivated_message": "@{acct} was deactivated",
|
||||||
"admin.users.user_deleted_message": "@{acct} was deleted",
|
"admin.users.user_deleted_message": "@{acct} was deleted",
|
||||||
|
@ -642,6 +644,10 @@
|
||||||
"edit_federation.save": "Save",
|
"edit_federation.save": "Save",
|
||||||
"edit_federation.success": "{host} federation was updated",
|
"edit_federation.success": "{host} federation was updated",
|
||||||
"edit_federation.unlisted": "Force posts unlisted",
|
"edit_federation.unlisted": "Force posts unlisted",
|
||||||
|
"edit_identity.names_title": "Names",
|
||||||
|
"edit_identity.pending_names_title": "Requested Names",
|
||||||
|
"edit_identity.reason_placeholder": "Why do you want this name?",
|
||||||
|
"edit_identity.request": "Request",
|
||||||
"edit_password.header": "Change Password",
|
"edit_password.header": "Change Password",
|
||||||
"edit_profile.error": "Profile update failed",
|
"edit_profile.error": "Profile update failed",
|
||||||
"edit_profile.fields.accepts_email_list_label": "Subscribe to newsletter",
|
"edit_profile.fields.accepts_email_list_label": "Subscribe to newsletter",
|
||||||
|
@ -1159,6 +1165,7 @@
|
||||||
"nostr_signup.siwe.action": "Sign in with extension",
|
"nostr_signup.siwe.action": "Sign in with extension",
|
||||||
"nostr_signup.siwe.alt": "Sign in with key",
|
"nostr_signup.siwe.alt": "Sign in with key",
|
||||||
"nostr_signup.siwe.title": "Sign in",
|
"nostr_signup.siwe.title": "Sign in",
|
||||||
|
"notification.ditto:name_grant": "You were granted the name {acct}",
|
||||||
"notification.favourite": "{name} liked your post",
|
"notification.favourite": "{name} liked your post",
|
||||||
"notification.follow": "{name} followed you",
|
"notification.follow": "{name} followed you",
|
||||||
"notification.follow_request": "{name} has requested to follow you",
|
"notification.follow_request": "{name} has requested to follow you",
|
||||||
|
@ -1168,7 +1175,6 @@
|
||||||
"notification.mentioned": "{name} mentioned you",
|
"notification.mentioned": "{name} mentioned you",
|
||||||
"notification.move": "{name} moved to {targetName}",
|
"notification.move": "{name} moved to {targetName}",
|
||||||
"notification.name": "{link}{others}",
|
"notification.name": "{link}{others}",
|
||||||
"notification.others": "+ {count, plural, one {# other} other {# others}}",
|
|
||||||
"notification.pleroma:chat_mention": "{name} sent you a message",
|
"notification.pleroma:chat_mention": "{name} sent you a message",
|
||||||
"notification.pleroma:emoji_reaction": "{name} reacted to your post",
|
"notification.pleroma:emoji_reaction": "{name} reacted to your post",
|
||||||
"notification.pleroma:event_reminder": "An event you are participating in starts soon",
|
"notification.pleroma:event_reminder": "An event you are participating in starts soon",
|
||||||
|
@ -1176,8 +1182,10 @@
|
||||||
"notification.pleroma:participation_request": "{name} wants to join your event",
|
"notification.pleroma:participation_request": "{name} wants to join your event",
|
||||||
"notification.poll": "A poll you have voted in has ended",
|
"notification.poll": "A poll you have voted in has ended",
|
||||||
"notification.reblog": "{name} reposted your post",
|
"notification.reblog": "{name} reposted your post",
|
||||||
|
"notification.set_name": "Set name to {name}",
|
||||||
"notification.status": "{name} just posted",
|
"notification.status": "{name} just posted",
|
||||||
"notification.update": "{name} edited a post you interacted with",
|
"notification.update": "{name} edited a post you interacted with",
|
||||||
|
"notification.update_name_success": "Name updated successfully",
|
||||||
"notification.user_approved": "Welcome to {instance}!",
|
"notification.user_approved": "Welcome to {instance}!",
|
||||||
"notifications.filter.all": "All",
|
"notifications.filter.all": "All",
|
||||||
"notifications.filter.boosts": "Reposts",
|
"notifications.filter.boosts": "Reposts",
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const NotificationRecord = ImmutableRecord({
|
||||||
emoji: null as string | null, // pleroma:emoji_reaction
|
emoji: null as string | null, // pleroma:emoji_reaction
|
||||||
emoji_url: null as string | null, // pleroma:emoji_reaction
|
emoji_url: null as string | null, // pleroma:emoji_reaction
|
||||||
id: '',
|
id: '',
|
||||||
|
name: '', // ditto:name_grant
|
||||||
status: null as EmbeddedEntity<Status>,
|
status: null as EmbeddedEntity<Status>,
|
||||||
target: null as EmbeddedEntity<Account>, // move
|
target: null as EmbeddedEntity<Account>, // move
|
||||||
type: '',
|
type: '',
|
||||||
|
|
|
@ -19,6 +19,8 @@ import {
|
||||||
ADMIN_USERS_DELETE_SUCCESS,
|
ADMIN_USERS_DELETE_SUCCESS,
|
||||||
ADMIN_USERS_APPROVE_REQUEST,
|
ADMIN_USERS_APPROVE_REQUEST,
|
||||||
ADMIN_USERS_APPROVE_SUCCESS,
|
ADMIN_USERS_APPROVE_SUCCESS,
|
||||||
|
ADMIN_USERS_REJECT_REQUEST,
|
||||||
|
ADMIN_USERS_REJECT_SUCCESS,
|
||||||
} from 'soapbox/actions/admin';
|
} from 'soapbox/actions/admin';
|
||||||
import { normalizeAdminReport, normalizeAdminAccount } from 'soapbox/normalizers';
|
import { normalizeAdminReport, normalizeAdminAccount } from 'soapbox/normalizers';
|
||||||
import { normalizeId } from 'soapbox/utils/normalizers';
|
import { normalizeId } from 'soapbox/utils/normalizers';
|
||||||
|
@ -120,22 +122,18 @@ function importUsers(state: State, users: APIUser[], filters: Filter[], page: nu
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteUsers(state: State, accountIds: string[]): State {
|
function deleteUser(state: State, accountId: string): State {
|
||||||
return state.withMutations(state => {
|
return state.withMutations(state => {
|
||||||
accountIds.forEach(id => {
|
state.update('awaitingApproval', orderedSet => orderedSet.delete(accountId));
|
||||||
state.update('awaitingApproval', orderedSet => orderedSet.delete(id));
|
state.deleteIn(['users', accountId]);
|
||||||
state.deleteIn(['users', id]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function approveUsers(state: State, users: APIUser[]): State {
|
function approveUser(state: State, user: APIUser): State {
|
||||||
|
const normalizedUser = fixUser(user);
|
||||||
return state.withMutations(state => {
|
return state.withMutations(state => {
|
||||||
users.forEach(user => {
|
state.update('awaitingApproval', orderedSet => orderedSet.delete(user.id));
|
||||||
const normalizedUser = fixUser(user);
|
state.setIn(['users', user.id], normalizedUser);
|
||||||
state.update('awaitingApproval', orderedSet => orderedSet.delete(user.id));
|
|
||||||
state.setIn(['users', user.id], normalizedUser);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,11 +205,13 @@ export default function admin(state: State = ReducerRecord(), action: AnyAction)
|
||||||
return importUsers(state, action.users, action.filters, action.page);
|
return importUsers(state, action.users, action.filters, action.page);
|
||||||
case ADMIN_USERS_DELETE_REQUEST:
|
case ADMIN_USERS_DELETE_REQUEST:
|
||||||
case ADMIN_USERS_DELETE_SUCCESS:
|
case ADMIN_USERS_DELETE_SUCCESS:
|
||||||
return deleteUsers(state, action.accountIds);
|
case ADMIN_USERS_REJECT_REQUEST:
|
||||||
|
case ADMIN_USERS_REJECT_SUCCESS:
|
||||||
|
return deleteUser(state, action.accountId);
|
||||||
case ADMIN_USERS_APPROVE_REQUEST:
|
case ADMIN_USERS_APPROVE_REQUEST:
|
||||||
return state.update('awaitingApproval', set => set.subtract(action.accountIds));
|
return state.update('awaitingApproval', set => set.remove(action.accountId));
|
||||||
case ADMIN_USERS_APPROVE_SUCCESS:
|
case ADMIN_USERS_APPROVE_SUCCESS:
|
||||||
return approveUsers(state, action.users);
|
return approveUser(state, action.user);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { accountSchema } from './account';
|
||||||
|
|
||||||
|
const adminAccountSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
account: accountSchema,
|
||||||
|
username: z.string(),
|
||||||
|
domain: z.string(),
|
||||||
|
created_at: z.string().datetime(),
|
||||||
|
email: z.string().email().nullish().catch(null),
|
||||||
|
ip: z.string().ip().nullish(),
|
||||||
|
ips: z.string().ip().array().nullish(),
|
||||||
|
locale: z.string(),
|
||||||
|
invite_request: z.string().nullish(),
|
||||||
|
role: z.string().nullish(),
|
||||||
|
confirmed: z.boolean().catch(true),
|
||||||
|
approved: z.boolean().catch(true),
|
||||||
|
disabled: z.boolean().catch(false),
|
||||||
|
silenced: z.boolean().catch(false),
|
||||||
|
suspended: z.boolean().catch(false),
|
||||||
|
sensitized: z.boolean().catch(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
type AdminAccount = z.infer<typeof adminAccountSchema>;
|
||||||
|
|
||||||
|
export { adminAccountSchema, AdminAccount };
|
|
@ -10,7 +10,6 @@ const baseNotificationSchema = z.object({
|
||||||
created_at: z.string().datetime().catch(new Date().toUTCString()),
|
created_at: z.string().datetime().catch(new Date().toUTCString()),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
total_count: z.number().optional().catch(undefined), // TruthSocial
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mentionNotificationSchema = baseNotificationSchema.extend({
|
const mentionNotificationSchema = baseNotificationSchema.extend({
|
||||||
|
@ -82,6 +81,11 @@ const participationAcceptedNotificationSchema = baseNotificationSchema.extend({
|
||||||
status: statusSchema,
|
status: statusSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nameGrantNotificationSchema = baseNotificationSchema.extend({
|
||||||
|
type: z.literal('ditto:name_grant'),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
const notificationSchema = z.discriminatedUnion('type', [
|
const notificationSchema = z.discriminatedUnion('type', [
|
||||||
mentionNotificationSchema,
|
mentionNotificationSchema,
|
||||||
statusNotificationSchema,
|
statusNotificationSchema,
|
||||||
|
@ -97,6 +101,7 @@ const notificationSchema = z.discriminatedUnion('type', [
|
||||||
eventReminderNotificationSchema,
|
eventReminderNotificationSchema,
|
||||||
participationRequestNotificationSchema,
|
participationRequestNotificationSchema,
|
||||||
participationAcceptedNotificationSchema,
|
participationAcceptedNotificationSchema,
|
||||||
|
nameGrantNotificationSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type Notification = z.infer<typeof notificationSchema>;
|
type Notification = z.infer<typeof notificationSchema>;
|
||||||
|
|
|
@ -944,6 +944,9 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
resetPassword: v.software === PLEROMA,
|
resetPassword: v.software === PLEROMA,
|
||||||
|
|
||||||
|
/** Admin can revoke the user's identity (without deleting their account). */
|
||||||
|
revokeName: v.software === DITTO,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ability to post statuses in Markdown, BBCode, and HTML.
|
* Ability to post statuses in Markdown, BBCode, and HTML.
|
||||||
* @see POST /api/v1/statuses
|
* @see POST /api/v1/statuses
|
||||||
|
|
|
@ -17,6 +17,7 @@ const NOTIFICATION_TYPES = [
|
||||||
'pleroma:event_reminder',
|
'pleroma:event_reminder',
|
||||||
'pleroma:participation_request',
|
'pleroma:participation_request',
|
||||||
'pleroma:participation_accepted',
|
'pleroma:participation_accepted',
|
||||||
|
'ditto:name_grant',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** Notification types to exclude from the "All" filter by default. */
|
/** Notification types to exclude from the "All" filter by default. */
|
||||||
|
|
Ładowanie…
Reference in New Issue