From 47492bac9aed9c68178aac5df153ded606eae1b8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Mar 2024 11:19:23 -0500 Subject: [PATCH 01/10] EditProfile: use instance.domain in nip05 placeholder --- src/features/edit-profile/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/edit-profile/index.tsx b/src/features/edit-profile/index.tsx index 03fe961aa..a01fbe816 100644 --- a/src/features/edit-profile/index.tsx +++ b/src/features/edit-profile/index.tsx @@ -325,7 +325,7 @@ const EditProfile: React.FC = () => { type='text' value={data.nip05} onChange={handleTextChange('nip05')} - placeholder={intl.formatMessage(messages.nip05Placeholder, { domain: location.host })} + placeholder={intl.formatMessage(messages.nip05Placeholder, { domain: instance.domain })} /> )} From bd552ab084f2c42f02072691fb16016797ffa7a5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Mar 2024 11:20:02 -0500 Subject: [PATCH 02/10] Add EditIdentity page (wip) --- src/features/edit-identity/index.tsx | 64 ++++++++++++++++++++++++ src/features/settings/index.tsx | 4 ++ src/features/ui/index.tsx | 2 + src/features/ui/util/async-components.ts | 1 + 4 files changed, 71 insertions(+) create mode 100644 src/features/edit-identity/index.tsx diff --git a/src/features/edit-identity/index.tsx b/src/features/edit-identity/index.tsx new file mode 100644 index 000000000..2a8854fc8 --- /dev/null +++ b/src/features/edit-identity/index.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import List, { ListItem } from 'soapbox/components/list'; +import { Button, Column, HStack, Icon, Input } from 'soapbox/components/ui'; +import { useInstance, useOwnAccount } from 'soapbox/hooks'; + +interface IEditIdentity { +} + +const messages = defineMessages({ + title: { id: 'settings.edit_identity', defaultMessage: 'Identity' }, +}); + +const identifiers = [ + 'alex@alexgleason.me', + 'lunk@alexgleason.me', + 'yolo@alexgleason.me', +]; + +/** EditIdentity component. */ +const EditIdentity: React.FC = () => { + const intl = useIntl(); + const { account } = useOwnAccount(); + + if (!account) return null; + + return ( + + + {identifiers.map((identifier) => ( + { /* TODO */ }} + /> + ))} + }> + + + + + ); +}; + +const UsernameInput: React.FC> = (props) => { + const instance = useInstance(); + + return ( + + + {instance.domain} + + )} + {...props} + /> + ); +}; + +export default EditIdentity; \ No newline at end of file diff --git a/src/features/settings/index.tsx b/src/features/settings/index.tsx index 434445c4e..b426a0f96 100644 --- a/src/features/settings/index.tsx +++ b/src/features/settings/index.tsx @@ -20,6 +20,7 @@ const messages = defineMessages({ configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' }, deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' }, editProfile: { id: 'settings.edit_profile', defaultMessage: 'Edit Profile' }, + editIdentity: { id: 'settings.edit_identity', defaultMessage: 'Identity' }, exportData: { id: 'column.export_data', defaultMessage: 'Export data' }, importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' }, mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' }, @@ -65,6 +66,9 @@ const Settings = () => { {displayName} + + {account?.source?.nostr?.nip05} + diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index 4d7fdf793..8cae1fce5 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -137,6 +137,7 @@ import { ExternalLogin, LandingTimeline, BookmarkFolders, + EditIdentity, } from './util/async-components'; import GlobalHotkeys from './util/global-hotkeys'; import { WrappedRoute } from './util/react-router-helpers'; @@ -305,6 +306,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.scheduledStatuses && } + {features.exportData && } {features.importData && } {features.accountAliases && } diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index 4660eb047..e5de03050 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -167,3 +167,4 @@ export const NostrLoginModal = lazy(() => import('soapbox/features/ui/components export const BookmarkFolders = lazy(() => import('soapbox/features/bookmark-folders')); export const EditBookmarkFolderModal = lazy(() => import('soapbox/features/ui/components/modals/edit-bookmark-folder-modal')); export const SelectBookmarkFolderModal = lazy(() => import('soapbox/features/ui/components/modals/select-bookmark-folder-modal')); +export const EditIdentity = lazy(() => import('soapbox/features/edit-identity')); \ No newline at end of file From fb3af2f74b40e2780e2667e058444097289b593f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Mar 2024 11:57:27 -0500 Subject: [PATCH 03/10] UsernameInput: reuse i18n message from nip05 profile field --- src/features/edit-identity/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/features/edit-identity/index.tsx b/src/features/edit-identity/index.tsx index 2a8854fc8..0b26631d2 100644 --- a/src/features/edit-identity/index.tsx +++ b/src/features/edit-identity/index.tsx @@ -10,6 +10,7 @@ interface IEditIdentity { const messages = defineMessages({ title: { id: 'settings.edit_identity', defaultMessage: 'Identity' }, + username: { id: 'edit_profile.fields.nip05_label', defaultMessage: 'Username' }, }); const identifiers = [ @@ -36,7 +37,7 @@ const EditIdentity: React.FC = () => { onSelect={() => { /* TODO */ }} /> ))} - }> + }> @@ -45,11 +46,12 @@ const EditIdentity: React.FC = () => { }; const UsernameInput: React.FC> = (props) => { + const intl = useIntl(); const instance = useInstance(); return ( From 51978c83f089813f45f5a9a48f9e547a862fd5b1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Mar 2024 12:43:36 -0500 Subject: [PATCH 04/10] EditIdentity: save nip05 to profile --- src/features/edit-identity/index.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/features/edit-identity/index.tsx b/src/features/edit-identity/index.tsx index 0b26631d2..8efcdd1fd 100644 --- a/src/features/edit-identity/index.tsx +++ b/src/features/edit-identity/index.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; +import { patchMe } from 'soapbox/actions/me'; import List, { ListItem } from 'soapbox/components/list'; import { Button, Column, HStack, Icon, Input } from 'soapbox/components/ui'; -import { useInstance, useOwnAccount } from 'soapbox/hooks'; +import { useAppDispatch, useInstance, useOwnAccount } from 'soapbox/hooks'; +import toast from 'soapbox/toast'; interface IEditIdentity { } @@ -11,6 +13,8 @@ interface IEditIdentity { const messages = defineMessages({ title: { id: 'settings.edit_identity', defaultMessage: 'Identity' }, username: { id: 'edit_profile.fields.nip05_label', defaultMessage: 'Username' }, + success: { id: 'edit_profile.success', defaultMessage: 'Your profile has been successfully saved!' }, + error: { id: 'edit_profile.error', defaultMessage: 'Profile update failed' }, }); const identifiers = [ @@ -22,10 +26,20 @@ const identifiers = [ /** EditIdentity component. */ const EditIdentity: React.FC = () => { const intl = useIntl(); + const dispatch = useAppDispatch(); const { account } = useOwnAccount(); if (!account) return null; + const updateNip05 = async (nip05: string): Promise => { + try { + await dispatch(patchMe({ nip05 })); + toast.success(intl.formatMessage(messages.success)); + } catch (e) { + toast.error(intl.formatMessage(messages.error)); + } + }; + return ( @@ -33,8 +47,8 @@ const EditIdentity: React.FC = () => { { /* TODO */ }} + isSelected={account.source?.nostr?.nip05 === identifier} + onSelect={() => updateNip05(identifier)} /> ))} }> From 4370a772a6ee0f92eab916681f7b0987be6d2247 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Mar 2024 13:10:48 -0500 Subject: [PATCH 05/10] EditIdentity: add exclamation emoji for unverified name --- src/features/edit-identity/index.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/features/edit-identity/index.tsx b/src/features/edit-identity/index.tsx index 8efcdd1fd..8263e0ca9 100644 --- a/src/features/edit-identity/index.tsx +++ b/src/features/edit-identity/index.tsx @@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl'; import { patchMe } from 'soapbox/actions/me'; import List, { ListItem } from 'soapbox/components/list'; -import { Button, Column, HStack, Icon, Input } from 'soapbox/components/ui'; +import { Button, Column, Emoji, HStack, Icon, Input, Tooltip } from 'soapbox/components/ui'; import { useAppDispatch, useInstance, useOwnAccount } from 'soapbox/hooks'; import toast from 'soapbox/toast'; @@ -13,6 +13,7 @@ interface IEditIdentity { const messages = defineMessages({ title: { id: 'settings.edit_identity', defaultMessage: 'Identity' }, username: { id: 'edit_profile.fields.nip05_label', defaultMessage: 'Username' }, + 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' }, }); @@ -32,6 +33,7 @@ const EditIdentity: React.FC = () => { if (!account) return null; const updateNip05 = async (nip05: string): Promise => { + if (account.source?.nostr?.nip05 === nip05) return; try { await dispatch(patchMe({ nip05 })); toast.success(intl.formatMessage(messages.success)); @@ -46,7 +48,18 @@ const EditIdentity: React.FC = () => { {identifiers.map((identifier) => ( + {identifier} + {(account.source?.nostr?.nip05 === identifier && account.acct !== identifier) && ( + +
+ +
+
+ )} + + } isSelected={account.source?.nostr?.nip05 === identifier} onSelect={() => updateNip05(identifier)} /> From c02f23c322c1dafd0ecc34d226a59f6b2d9622fc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 27 Mar 2024 09:52:07 -0500 Subject: [PATCH 06/10] EditIdentity: pull identifiers from Nostr --- src/features/edit-identity/index.tsx | 84 ++++++++++++++++++---------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/src/features/edit-identity/index.tsx b/src/features/edit-identity/index.tsx index 8263e0ca9..8cd48db22 100644 --- a/src/features/edit-identity/index.tsx +++ b/src/features/edit-identity/index.tsx @@ -1,9 +1,11 @@ -import React from 'react'; +import React, { useState } from 'react'; import { 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 toast from 'soapbox/toast'; @@ -18,17 +20,22 @@ const messages = defineMessages({ error: { id: 'edit_profile.error', defaultMessage: 'Profile update failed' }, }); -const identifiers = [ - 'alex@alexgleason.me', - 'lunk@alexgleason.me', - 'yolo@alexgleason.me', -]; - /** EditIdentity component. */ const EditIdentity: React.FC = () => { const intl = useIntl(); + const instance = useInstance(); const dispatch = useAppDispatch(); const { account } = useOwnAccount(); + const { relay, signer } = useNostr(); + + const admin = instance.nostr?.pubkey; + const [username, setUsername] = useState(''); + + const { events: labels } = useNostrReq( + admin + ? [{ kinds: [1985], authors: [admin], '#L': ['nip05'] }] + : [], + ); if (!account) return null; @@ -42,30 +49,51 @@ const EditIdentity: React.FC = () => { } }; + 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); + }; + return ( - {identifiers.map((identifier) => ( - - {identifier} - {(account.source?.nostr?.nip05 === identifier && account.acct !== identifier) && ( - -
- -
-
- )} - - } - isSelected={account.source?.nostr?.nip05 === identifier} - onSelect={() => updateNip05(identifier)} - /> - ))} - }> - + {labels.map((label) => { + const identifier = label.tags.find(([name]) => name === 'l')?.[1]; + if (!identifier) return null; + + return ( + + {identifier} + {(account.source?.nostr?.nip05 === identifier && account.acct !== identifier) && ( + +
+ +
+
+ )} + + } + isSelected={account.source?.nostr?.nip05 === identifier} + onSelect={() => updateNip05(identifier)} + /> + ); + })} + setUsername(e.target.value)} />}> +
From 37816b0a8c8d57c09b8b6761378e4568ee8dfe28 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 27 Mar 2024 09:53:45 -0500 Subject: [PATCH 07/10] EditIdentity: only pull current user's pubkey --- src/features/edit-identity/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/edit-identity/index.tsx b/src/features/edit-identity/index.tsx index 8cd48db22..c5dc68ab3 100644 --- a/src/features/edit-identity/index.tsx +++ b/src/features/edit-identity/index.tsx @@ -29,11 +29,12 @@ const EditIdentity: React.FC = () => { const { relay, signer } = useNostr(); const admin = instance.nostr?.pubkey; + const pubkey = account?.nostr?.pubkey; const [username, setUsername] = useState(''); const { events: labels } = useNostrReq( - admin - ? [{ kinds: [1985], authors: [admin], '#L': ['nip05'] }] + (admin && pubkey) + ? [{ kinds: [1985], authors: [admin], '#L': ['nip05'], '#p': [pubkey] }] : [], ); From bd46f1230b7c813efebc64e6c3685b0e19240c82 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 27 Mar 2024 15:19:00 -0500 Subject: [PATCH 08/10] useNostrReq: deduplicate events with NSet --- src/features/nostr/hooks/useNostrReq.ts | 12 ++++++++---- src/hooks/useForceUpdate.ts | 11 +++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 src/hooks/useForceUpdate.ts diff --git a/src/features/nostr/hooks/useNostrReq.ts b/src/features/nostr/hooks/useNostrReq.ts index 654c814f3..72e1187ac 100644 --- a/src/features/nostr/hooks/useNostrReq.ts +++ b/src/features/nostr/hooks/useNostrReq.ts @@ -1,14 +1,17 @@ -import { NostrEvent, NostrFilter } from '@soapbox/nspec'; +import { NSet, NostrEvent, NostrFilter } from '@soapbox/nspec'; import isEqual from 'lodash/isEqual'; import { useEffect, useRef, useState } from 'react'; import { useNostr } from 'soapbox/contexts/nostr-context'; +import { useForceUpdate } from 'soapbox/hooks/useForceUpdate'; /** Streams events from the relay for the given filters. */ export function useNostrReq(filters: NostrFilter[]): { events: NostrEvent[]; eose: boolean; closed: boolean } { const { relay } = useNostr(); - const [events, setEvents] = useState([]); + const nset = useRef(new NSet()); + const forceUpdate = useForceUpdate(); + const [closed, setClosed] = useState(false); const [eose, setEose] = useState(false); @@ -21,7 +24,8 @@ export function useNostrReq(filters: NostrFilter[]): { events: NostrEvent[]; eos (async () => { for await (const msg of relay.req(value, { signal })) { if (msg[0] === 'EVENT') { - setEvents((prev) => [msg[2], ...prev]); + nset.current.add(msg[2]); + forceUpdate(); } else if (msg[0] === 'EOSE') { setEose(true); } else if (msg[0] === 'CLOSED') { @@ -41,7 +45,7 @@ export function useNostrReq(filters: NostrFilter[]): { events: NostrEvent[]; eos }, [relay, value]); return { - events, + events: [...nset.current], eose, closed, }; diff --git a/src/hooks/useForceUpdate.ts b/src/hooks/useForceUpdate.ts new file mode 100644 index 000000000..50a84a468 --- /dev/null +++ b/src/hooks/useForceUpdate.ts @@ -0,0 +1,11 @@ +import { useState, useCallback } from 'react'; + +export function useForceUpdate(): () => void { + const [, setState] = useState(false); + + const forceUpdate = useCallback(() => { + setState(prevState => !prevState); + }, []); + + return forceUpdate; +} From aea4c216896feefed85a8b7f4e33e84d97926ece Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 27 Mar 2024 17:52:34 -0500 Subject: [PATCH 09/10] Feature-gate EditIdentity screen --- src/features/settings/index.tsx | 8 +++++--- src/features/ui/index.tsx | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/features/settings/index.tsx b/src/features/settings/index.tsx index b426a0f96..f04f311a8 100644 --- a/src/features/settings/index.tsx +++ b/src/features/settings/index.tsx @@ -66,9 +66,11 @@ const Settings = () => { {displayName} - - {account?.source?.nostr?.nip05} - + {features.nip05 && ( + + {account?.source?.nostr?.nip05} + + )}
diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index 527b3d915..b206a647b 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -307,7 +307,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.scheduledStatuses && } - + {features.nip05 && } {features.exportData && } {features.importData && } {features.accountAliases && } From 06beaa54de9d1a3dc68c6c2d9307f9340928088b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 28 Mar 2024 13:48:38 -0500 Subject: [PATCH 10/10] yarn i18n --- src/locales/en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/locales/en.json b/src/locales/en.json index 3bd8d6740..f1189955b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -634,6 +634,7 @@ "edit_profile.fields.meta_fields_label": "Profile fields", "edit_profile.fields.nip05_label": "Username", "edit_profile.fields.nip05_placeholder": "user@{domain}", + "edit_profile.fields.nip05_unverified": "Name could not be verified and won't be used.", "edit_profile.fields.stranger_notifications_label": "Block notifications from strangers", "edit_profile.fields.website_label": "Website", "edit_profile.fields.website_placeholder": "Display a Link", @@ -1376,6 +1377,7 @@ "settings.change_password": "Change Password", "settings.configure_mfa": "Configure MFA", "settings.delete_account": "Delete Account", + "settings.edit_identity": "Identity", "settings.edit_profile": "Edit Profile", "settings.messages.label": "Allow users to start a new chat with you", "settings.mutes": "Mutes",