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 ( + + + + ); +}; + +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..e92f2dabe --- /dev/null +++ b/src/features/nostr-relays/index.tsx @@ -0,0 +1,74 @@ +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'; + +const messages = defineMessages({ + title: { id: 'nostr_relays.title', defaultMessage: 'Relays' }, +}); + +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); + + 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); + + 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); + }; + + return ( + +
+ + + + + + + + + +
+
+ ); +}; + +export default NostrRelays; 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/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 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", 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