diff --git a/package.json b/package.json index bde744cfc..2ffe7dfce 100644 --- a/package.json +++ b/package.json @@ -55,12 +55,12 @@ "@fontsource/roboto-mono": "^5.0.0", "@fontsource/tajawal": "^5.0.8", "@gamestdio/websocket": "^0.3.2", - "@lexical/clipboard": "^0.13.1", - "@lexical/hashtag": "^0.13.1", - "@lexical/link": "^0.13.1", - "@lexical/react": "^0.13.1", - "@lexical/selection": "^0.13.1", - "@lexical/utils": "^0.13.1", + "@lexical/clipboard": "^0.14.2", + "@lexical/hashtag": "^0.14.2", + "@lexical/link": "^0.14.2", + "@lexical/react": "^0.14.2", + "@lexical/selection": "^0.14.2", + "@lexical/utils": "^0.14.2", "@mkljczk/react-hotkeys": "^1.2.2", "@noble/hashes": "^1.3.3", "@popperjs/core": "^2.11.5", @@ -131,7 +131,7 @@ "intl-pluralrules": "^2.0.0", "isomorphic-dompurify": "^2.3.0", "leaflet": "^1.8.0", - "lexical": "^0.13.1", + "lexical": "^0.14.2", "line-awesome": "^1.3.0", "localforage": "^1.10.0", "lodash": "^4.7.11", diff --git a/src/actions/remote-timeline.ts b/src/actions/remote-timeline.ts index cb21126b9..ac1277aec 100644 --- a/src/actions/remote-timeline.ts +++ b/src/actions/remote-timeline.ts @@ -1,11 +1,11 @@ import { getSettings, changeSetting } from 'soapbox/actions/settings'; -import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import type { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; import type { AppDispatch, RootState } from 'soapbox/store'; const getPinnedHosts = (state: RootState) => { const settings = getSettings(state); - return settings.getIn(['remote_timeline', 'pinnedHosts']) as ImmutableOrderedSet; + return settings.getIn(['remote_timeline', 'pinnedHosts']) as ImmutableList | ImmutableOrderedSet; }; const pinHost = (host: string) => @@ -13,7 +13,7 @@ const pinHost = (host: string) => const state = getState(); const pinnedHosts = getPinnedHosts(state); - return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.add(host))); + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.toOrderedSet().add(host))); }; const unpinHost = (host: string) => @@ -21,7 +21,7 @@ const unpinHost = (host: string) => const state = getState(); const pinnedHosts = getPinnedHosts(state); - return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.remove(host))); + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.toOrderedSet().remove(host))); }; export { diff --git a/src/actions/settings.ts b/src/actions/settings.ts index ae6a3ec35..ae26af50a 100644 --- a/src/actions/settings.ts +++ b/src/actions/settings.ts @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { defineMessage } from 'react-intl'; import { createSelector } from 'reselect'; import { v4 as uuid } from 'uuid'; @@ -169,7 +169,7 @@ const defaultSettings = ImmutableMap({ ]), remote_timeline: ImmutableMap({ - pinnedHosts: ImmutableOrderedSet(), + pinnedHosts: ImmutableList(), }), }); 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 ( + + {instance.domain} + + )} + {...props} + /> + ); +}; + +export default EditIdentity; \ No newline at end of file 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 })} /> )} diff --git a/src/features/federation-restrictions/components/instance-restrictions.tsx b/src/features/federation-restrictions/components/instance-restrictions.tsx index bb0b2c2ec..344dc766d 100644 --- a/src/features/federation-restrictions/components/instance-restrictions.tsx +++ b/src/features/federation-restrictions/components/instance-restrictions.tsx @@ -5,10 +5,10 @@ import Icon from 'soapbox/components/icon'; import { HStack, Stack, Text } from 'soapbox/components/ui'; import { useInstance } from 'soapbox/hooks'; -import type { Map as ImmutableMap } from 'immutable'; +import type { RemoteInstance } from 'soapbox/selectors'; -const hasRestrictions = (remoteInstance: ImmutableMap): boolean => { - const { accept, reject_deletes, report_removal, ...federation } = remoteInstance.get('federation'); +const hasRestrictions = (remoteInstance: RemoteInstance): boolean => { + const { accept, reject_deletes, report_removal, ...federation } = remoteInstance.federation; return !!Object.values(federation).reduce((acc, value) => Boolean(acc || value), false); }; @@ -30,7 +30,7 @@ const Restriction: React.FC = ({ icon, children }) => { }; interface IInstanceRestrictions { - remoteInstance: ImmutableMap; + remoteInstance: RemoteInstance; } const InstanceRestrictions: React.FC = ({ remoteInstance }) => { @@ -46,7 +46,7 @@ const InstanceRestrictions: React.FC = ({ remoteInstance followers_only, media_nsfw, media_removal, - } = remoteInstance.get('federation').toJS(); + } = remoteInstance.federation; const fullMediaRemoval = media_removal && avatar_removal && banner_removal; const partialMediaRemoval = media_removal || avatar_removal || banner_removal; @@ -108,10 +108,10 @@ const InstanceRestrictions: React.FC = ({ remoteInstance const renderContent = () => { if (!instance || !remoteInstance) return null; - const host = remoteInstance.get('host'); + const host = remoteInstance.host; const siteTitle = instance.title; - if (remoteInstance.getIn(['federation', 'reject']) === true) { + if (remoteInstance.federation.reject === true) { return ( = ({ host }) => {
-
- {remoteInstance.get('host')} +
+ {remoteInstance.host}
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 }); + }; + }; + + const handleMarkerChange = (e: React.ChangeEvent): void => { + onChange({ ...value, marker: (e.currentTarget.value as 'read' | 'write' | '') || undefined }); + }; + + 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..e3842f2dc --- /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[0]]); + + 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/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/features/remote-timeline/components/pinned-hosts-picker.tsx b/src/features/remote-timeline/components/pinned-hosts-picker.tsx index 2777aa6e3..16cd45481 100644 --- a/src/features/remote-timeline/components/pinned-hosts-picker.tsx +++ b/src/features/remote-timeline/components/pinned-hosts-picker.tsx @@ -16,7 +16,7 @@ const PinnedHostsPicker: React.FC = ({ host: activeHost }) = return ( - {pinnedHosts.map((host: any) => ( + {pinnedHosts.map((host) => (