Rework identity request to match the new API

environments/review-main-yi2y9f/deployments/4692^2
Alex Gleason 3 months ago
parent def7cc4d14
commit 4927a321df
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7

@ -1,12 +1,12 @@
import { useMutation, useQuery } from '@tanstack/react-query';
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 List, { ListItem } from 'soapbox/components/list';
import { Button, Column, Emoji, HStack, Icon, Input, Tooltip } from 'soapbox/components/ui';
import { useNostr } from 'soapbox/contexts/nostr-context';
import { useNostrReq } from 'soapbox/features/nostr/hooks/useNostrReq';
import { useAppDispatch, useInstance, useOwnAccount } from 'soapbox/hooks';
import { Button, CardHeader, CardTitle, Column, Emoji, Form, HStack, Icon, Input, Textarea, Tooltip } from 'soapbox/components/ui';
import { useApi, useAppDispatch, useInstance, useOwnAccount } from 'soapbox/hooks';
import { adminAccountSchema } from 'soapbox/schemas/admin-account';
import toast from 'soapbox/toast';
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.' },
success: { id: 'edit_profile.success', defaultMessage: 'Your profile has been successfully saved!' },
error: { id: 'edit_profile.error', defaultMessage: 'Profile update failed' },
placeholder: { id: 'edit_identity.reason_placeholder', defaultMessage: 'Why do you want this name?' },
});
/** EditIdentity component. */
@ -26,77 +27,111 @@ const EditIdentity: React.FC<IEditIdentity> = () => {
const instance = useInstance();
const dispatch = useAppDispatch();
const { account } = useOwnAccount();
const { relay, signer } = useNostr();
const { mutate } = useRequestName();
const admin = instance.nostr?.pubkey;
const pubkey = account?.nostr?.pubkey;
const [username, setUsername] = useState<string>('');
const { data: approvedNames } = useNames();
const { data: pendingNames } = usePendingNames();
const { events: labels } = useNostrReq(
(admin && pubkey)
? [{ kinds: [1985], authors: [admin], '#L': ['nip05'], '#p': [pubkey] }]
: [],
);
const [username, setUsername] = useState<string>('');
const [reason, setReason] = useState<string>('');
if (!account) return null;
const updateNip05 = async (nip05: string): Promise<void> => {
if (account.source?.nostr?.nip05 === nip05) return;
const updateName = async (name: string): Promise<void> => {
if (account.source?.nostr?.nip05 === name) return;
try {
await dispatch(patchMe({ nip05 }));
await dispatch(patchMe({ nip05: name }));
toast.success(intl.formatMessage(messages.success));
} catch (e) {
toast.error(intl.formatMessage(messages.error));
}
};
const submit = async () => {
if (!admin || !signer || !relay) return;
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);
const submit = () => {
const name = `${username}@${instance.domain}`;
mutate({ name, reason });
};
return (
<Column label={intl.formatMessage(messages.title)}>
<List>
{labels.map((label) => {
const identifier = label.tags.find(([name]) => name === 'l')?.[1];
if (!identifier) return null;
return (
<ListItem
key={identifier}
label={
<HStack alignItems='center' space={2}>
<span>{identifier}</span>
{(account.source?.nostr?.nip05 === identifier && account.acct !== identifier) && (
<Tooltip text={intl.formatMessage(messages.unverified)}>
<div>
<Emoji className='h-4 w-4' emoji='⚠️' />
</div>
</Tooltip>
)}
</HStack>
}
isSelected={account.source?.nostr?.nip05 === identifier}
onSelect={() => updateNip05(identifier)}
/>
);
})}
<ListItem label={<UsernameInput value={username} onChange={(e) => setUsername(e.target.value)} />}>
<Button theme='accent' onClick={submit}>Add</Button>
</ListItem>
</List>
<div className='space-y-4'>
<Form>
<UsernameInput value={username} onChange={(e) => setUsername(e.target.value)} />
<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>
{((approvedNames?.length ?? 0) > 0) && (
<>
<CardHeader>
<CardTitle title={<FormattedMessage id='edit_identity.names_title' defaultMessage='Names' />} />
</CardHeader>
<List>
{approvedNames?.map(({ username, domain }) => {
const identifier = `${username}@${domain}`;
if (!identifier) return null;
return (
<ListItem
key={identifier}
label={
<HStack alignItems='center' space={2}>
<span>{identifier}</span>
{(account.source?.nostr?.nip05 === identifier && account.acct !== identifier) && (
<Tooltip text={intl.formatMessage(messages.unverified)}>
<div>
<Emoji className='h-4 w-4' emoji='⚠️' />
</div>
</Tooltip>
)}
</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>
);
};
@ -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;

@ -1159,6 +1159,7 @@
"nostr_signup.siwe.action": "Sign in with extension",
"nostr_signup.siwe.alt": "Sign in with key",
"nostr_signup.siwe.title": "Sign in",
"notification.ditto:name_grant": "You were granted the name {acct}",
"notification.favourite": "{name} liked your post",
"notification.follow": "{name} followed you",
"notification.follow_request": "{name} has requested to follow you",
@ -1168,7 +1169,6 @@
"notification.mentioned": "{name} mentioned you",
"notification.move": "{name} moved to {targetName}",
"notification.name": "{link}{others}",
"notification.others": "+ {count, plural, one {# other} other {# others}}",
"notification.pleroma:chat_mention": "{name} sent you a message",
"notification.pleroma:emoji_reaction": "{name} reacted to your post",
"notification.pleroma:event_reminder": "An event you are participating in starts soon",
@ -1176,8 +1176,10 @@
"notification.pleroma:participation_request": "{name} wants to join your event",
"notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} reposted your post",
"notification.set_name": "Set name to {name}",
"notification.status": "{name} just posted",
"notification.update": "{name} edited a post you interacted with",
"notification.update_name_success": "Name updated successfully",
"notification.user_approved": "Welcome to {instance}!",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Reposts",

@ -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 };
Loading…
Cancel
Save