From db9f65cf7786f94451c7e2c860392b3c627d6cb0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 1 Apr 2024 12:09:47 -0500 Subject: [PATCH 1/4] Add basic Nostr relay editor UI --- .../nostr-relays/components/relay-editor.tsx | 63 +++++++++++++++++++ src/features/nostr-relays/index.tsx | 45 +++++++++++++ src/features/ui/index.tsx | 2 + src/features/ui/util/async-components.ts | 1 + 4 files changed, 111 insertions(+) create mode 100644 src/features/nostr-relays/components/relay-editor.tsx create mode 100644 src/features/nostr-relays/index.tsx diff --git a/src/features/nostr-relays/components/relay-editor.tsx b/src/features/nostr-relays/components/relay-editor.tsx new file mode 100644 index 000000000..6f6a5ae41 --- /dev/null +++ b/src/features/nostr-relays/components/relay-editor.tsx @@ -0,0 +1,63 @@ +import React from 'react'; + +import { HStack, Input } from 'soapbox/components/ui'; +import Streamfield, { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield'; +import { useInstance } from 'soapbox/hooks'; + +interface IRelayEditor { + relays: RelayData[]; + setRelays: (relays: RelayData[]) => void; +} + +const RelayEditor: React.FC = ({ relays, setRelays }) => { + const handleAddRelay = (): void => { + setRelays([...relays, { url: '' }]); + }; + + const handleRemoveRelay = (i: number): void => { + const newRelays = [...relays]; + newRelays.splice(i, 1); + setRelays(newRelays); + }; + + return ( + + ); +}; + +interface RelayData { + url: string; + marker?: 'read' | 'write'; +} + +const RelayField: StreamfieldComponent = ({ value, onChange }) => { + const instance = useInstance(); + + const handleChange = (key: string): React.ChangeEventHandler => { + return e => { + onChange({ ...value, [key]: e.currentTarget.value }); + }; + }; + + return ( + + + + ); +}; + +export default RelayEditor; + +export type { RelayData }; \ No newline at end of file diff --git a/src/features/nostr-relays/index.tsx b/src/features/nostr-relays/index.tsx new file mode 100644 index 000000000..2921390e7 --- /dev/null +++ b/src/features/nostr-relays/index.tsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; + +import { Button, Column, Form, FormActions, Stack } from 'soapbox/components/ui'; + +import RelayEditor, { RelayData } from './components/relay-editor'; + +const messages = defineMessages({ + title: { id: 'nostr_relays.title', defaultMessage: 'Relays' }, +}); + +const NostrRelays = () => { + const intl = useIntl(); + + const [relays, setRelays] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = (): void => { + setIsLoading(true); + // Save relays + setIsLoading(false); + }; + + return ( + +
+ + + + + + + + + +
+
+ ); +}; + +export default NostrRelays; diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index b206a647b..5574196ab 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -139,6 +139,7 @@ import { BookmarkFolders, EditIdentity, Domains, + NostrRelays, } from './util/async-components'; import GlobalHotkeys from './util/global-hotkeys'; import { WrappedRoute } from './util/react-router-helpers'; @@ -313,6 +314,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.accountAliases && } {features.accountMoving && } {features.backups && } + diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index b6dff63f3..c9fae695d 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -170,3 +170,4 @@ export const SelectBookmarkFolderModal = lazy(() => import('soapbox/features/ui/ export const EditIdentity = lazy(() => import('soapbox/features/edit-identity')); export const Domains = lazy(() => import('soapbox/features/admin/domains')); export const EditDomainModal = lazy(() => import('soapbox/features/ui/components/modals/edit-domain-modal')); +export const NostrRelays = lazy(() => import('soapbox/features/nostr-relays')); \ No newline at end of file From bc6082d3f213abe77cc288b5b64a722eb30a40a2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 1 Apr 2024 13:19:19 -0500 Subject: [PATCH 2/4] RelayEditor: allow setting the marker --- src/components/ui/select/select.tsx | 8 +++++-- .../nostr-relays/components/relay-editor.tsx | 21 +++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/components/ui/select/select.tsx b/src/components/ui/select/select.tsx index b67ebe975..b74f8195e 100644 --- a/src/components/ui/select/select.tsx +++ b/src/components/ui/select/select.tsx @@ -3,18 +3,22 @@ import React from 'react'; interface ISelect extends React.SelectHTMLAttributes { children: Iterable; + full?: boolean; } /** Multiple-select dropdown. */ const Select = React.forwardRef((props, ref) => { - const { children, className, ...filteredProps } = props; + const { children, className, full = true, ...filteredProps } = props; return ( + + ); }; From 20f60e763c17a57c463e947412b13b2bcd13b494 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 2 Apr 2024 01:54:55 -0500 Subject: [PATCH 3/4] edit relays: hook up data --- src/features/nostr-relays/index.tsx | 35 ++++++++++++++++++++++++++--- src/features/settings/index.tsx | 2 ++ src/utils/features.ts | 3 +++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/features/nostr-relays/index.tsx b/src/features/nostr-relays/index.tsx index 2921390e7..e92f2dabe 100644 --- a/src/features/nostr-relays/index.tsx +++ b/src/features/nostr-relays/index.tsx @@ -1,7 +1,10 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { Button, Column, Form, FormActions, Stack } from 'soapbox/components/ui'; +import { useNostr } from 'soapbox/contexts/nostr-context'; +import { useNostrReq } from 'soapbox/features/nostr/hooks/useNostrReq'; +import { useOwnAccount } from 'soapbox/hooks'; import RelayEditor, { RelayData } from './components/relay-editor'; @@ -11,13 +14,39 @@ const messages = defineMessages({ const NostrRelays = () => { const intl = useIntl(); + const { account } = useOwnAccount(); + const { relay, signer } = useNostr(); + + const { events } = useNostrReq( + account?.nostr + ? [{ kinds: [10002], authors: [account?.nostr.pubkey], limit: 1 }] + : [], + ); const [relays, setRelays] = useState([]); const [isLoading, setIsLoading] = useState(false); - const handleSubmit = (): void => { + useEffect(() => { + const tags = events[0]?.tags ?? []; + const data = tags.map(tag => ({ url: tag[1], marker: tag[2] as 'read' | 'write' | undefined })); + setRelays(data); + }, [events]); + + const handleSubmit = async (): Promise => { + if (!signer || !relay) return; + setIsLoading(true); - // Save relays + + const event = await signer.signEvent({ + kind: 10002, + tags: relays.map(relay => relay.marker ? ['r', relay.url, relay.marker] : ['r', relay.url]), + content: '', + created_at: Math.floor(Date.now() / 1000), + }); + + // eslint-disable-next-line compat/compat + await relay.event(event, { signal: AbortSignal.timeout(1000) }); + setIsLoading(false); }; diff --git a/src/features/settings/index.tsx b/src/features/settings/index.tsx index f04f311a8..f2e53b867 100644 --- a/src/features/settings/index.tsx +++ b/src/features/settings/index.tsx @@ -21,6 +21,7 @@ const messages = defineMessages({ deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' }, editProfile: { id: 'settings.edit_profile', defaultMessage: 'Edit Profile' }, editIdentity: { id: 'settings.edit_identity', defaultMessage: 'Identity' }, + editRelays: { id: 'nostr_relays.title', defaultMessage: 'Relays' }, exportData: { id: 'column.export_data', defaultMessage: 'Export data' }, importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' }, mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' }, @@ -71,6 +72,7 @@ const Settings = () => { {account?.source?.nostr?.nip05} )} + {features.nostr && } diff --git a/src/utils/features.ts b/src/utils/features.ts index 37c7af53b..8c42da8f4 100644 --- a/src/utils/features.ts +++ b/src/utils/features.ts @@ -757,6 +757,9 @@ const getInstanceFeatures = (instance: Instance) => { */ nip05: v.software === DITTO, + /** Has a Nostr relay. */ + nostr: !!instance.nostr?.relay, + /** * Ability to sign Nostr events over websocket. * @see GET /api/v1/streaming?stream=nostr From 569f7e5b3621de3215cfab1f9d2768a40fd6a527 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 2 Apr 2024 01:56:29 -0500 Subject: [PATCH 4/4] yarn i18n --- src/locales/en.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/locales/en.json b/src/locales/en.json index f1189955b..83851b3bc 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1115,6 +1115,10 @@ "new_group_panel.title": "Create Group", "nostr_extension.found": "Sign in with browser extension.", "nostr_extension.not_found": "Browser extension not found.", + "nostr_relays.read_only": "Read-only", + "nostr_relays.read_write": "Read & write", + "nostr_relays.title": "Relays", + "nostr_relays.write_only": "Write-only", "nostr_signup.key-add.title": "Import Key", "nostr_signup.key.title": "You need a key to continue", "nostr_signup.keygen.title": "Your new key",