Merge remote-tracking branch 'origin/main' into drafts

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-04-02 23:11:30 +02:00
commit 8c36432a06
22 changed files with 489 additions and 164 deletions

View file

@ -55,12 +55,12 @@
"@fontsource/roboto-mono": "^5.0.0", "@fontsource/roboto-mono": "^5.0.0",
"@fontsource/tajawal": "^5.0.8", "@fontsource/tajawal": "^5.0.8",
"@gamestdio/websocket": "^0.3.2", "@gamestdio/websocket": "^0.3.2",
"@lexical/clipboard": "^0.13.1", "@lexical/clipboard": "^0.14.2",
"@lexical/hashtag": "^0.13.1", "@lexical/hashtag": "^0.14.2",
"@lexical/link": "^0.13.1", "@lexical/link": "^0.14.2",
"@lexical/react": "^0.13.1", "@lexical/react": "^0.14.2",
"@lexical/selection": "^0.13.1", "@lexical/selection": "^0.14.2",
"@lexical/utils": "^0.13.1", "@lexical/utils": "^0.14.2",
"@mkljczk/react-hotkeys": "^1.2.2", "@mkljczk/react-hotkeys": "^1.2.2",
"@noble/hashes": "^1.3.3", "@noble/hashes": "^1.3.3",
"@popperjs/core": "^2.11.5", "@popperjs/core": "^2.11.5",
@ -131,7 +131,7 @@
"intl-pluralrules": "^2.0.0", "intl-pluralrules": "^2.0.0",
"isomorphic-dompurify": "^2.3.0", "isomorphic-dompurify": "^2.3.0",
"leaflet": "^1.8.0", "leaflet": "^1.8.0",
"lexical": "^0.13.1", "lexical": "^0.14.2",
"line-awesome": "^1.3.0", "line-awesome": "^1.3.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.7.11", "lodash": "^4.7.11",

View file

@ -1,11 +1,11 @@
import { getSettings, changeSetting } from 'soapbox/actions/settings'; 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'; import type { AppDispatch, RootState } from 'soapbox/store';
const getPinnedHosts = (state: RootState) => { const getPinnedHosts = (state: RootState) => {
const settings = getSettings(state); const settings = getSettings(state);
return settings.getIn(['remote_timeline', 'pinnedHosts']) as ImmutableOrderedSet<string>; return settings.getIn(['remote_timeline', 'pinnedHosts']) as ImmutableList<string> | ImmutableOrderedSet<string>;
}; };
const pinHost = (host: string) => const pinHost = (host: string) =>
@ -13,7 +13,7 @@ const pinHost = (host: string) =>
const state = getState(); const state = getState();
const pinnedHosts = getPinnedHosts(state); 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) => const unpinHost = (host: string) =>
@ -21,7 +21,7 @@ const unpinHost = (host: string) =>
const state = getState(); const state = getState();
const pinnedHosts = getPinnedHosts(state); const pinnedHosts = getPinnedHosts(state);
return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.remove(host))); return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.toOrderedSet().remove(host)));
}; };
export { export {

View file

@ -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 { defineMessage } from 'react-intl';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@ -169,7 +169,7 @@ const defaultSettings = ImmutableMap({
]), ]),
remote_timeline: ImmutableMap({ remote_timeline: ImmutableMap({
pinnedHosts: ImmutableOrderedSet(), pinnedHosts: ImmutableList(),
}), }),
}); });

View file

@ -3,18 +3,22 @@ import React from 'react';
interface ISelect extends React.SelectHTMLAttributes<HTMLSelectElement> { interface ISelect extends React.SelectHTMLAttributes<HTMLSelectElement> {
children: Iterable<React.ReactNode>; children: Iterable<React.ReactNode>;
full?: boolean;
} }
/** Multiple-select dropdown. */ /** Multiple-select dropdown. */
const Select = React.forwardRef<HTMLSelectElement, ISelect>((props, ref) => { const Select = React.forwardRef<HTMLSelectElement, ISelect>((props, ref) => {
const { children, className, ...filteredProps } = props; const { children, className, full = true, ...filteredProps } = props;
return ( return (
<select <select
ref={ref} ref={ref}
className={clsx( className={clsx(
'w-full truncate rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-primary-500 focus:outline-none focus:ring-primary-500 disabled:opacity-50 black:bg-black sm:text-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:border-primary-500 dark:focus:ring-primary-500', 'truncate rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-primary-500 focus:outline-none focus:ring-primary-500 disabled:opacity-50 black:bg-black sm:text-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:border-primary-500 dark:focus:ring-primary-500',
className, className,
{
'w-full': full,
},
)} )}
{...filteredProps} {...filteredProps}
> >

View file

@ -0,0 +1,122 @@
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';
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' },
});
/** EditIdentity component. */
const EditIdentity: React.FC<IEditIdentity> = () => {
const intl = useIntl();
const instance = useInstance();
const dispatch = useAppDispatch();
const { account } = useOwnAccount();
const { relay, signer } = useNostr();
const admin = instance.nostr?.pubkey;
const pubkey = account?.nostr?.pubkey;
const [username, setUsername] = useState<string>('');
const { events: labels } = useNostrReq(
(admin && pubkey)
? [{ kinds: [1985], authors: [admin], '#L': ['nip05'], '#p': [pubkey] }]
: [],
);
if (!account) return null;
const updateNip05 = async (nip05: string): Promise<void> => {
if (account.source?.nostr?.nip05 === nip05) return;
try {
await dispatch(patchMe({ nip05 }));
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);
};
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>
</Column>
);
};
const UsernameInput: React.FC<React.ComponentProps<typeof Input>> = (props) => {
const intl = useIntl();
const instance = useInstance();
return (
<Input
placeholder={intl.formatMessage(messages.username)}
append={(
<HStack alignItems='center' space={1} className='rounded p-1 text-sm backdrop-blur'>
<Icon className='h-4 w-4' src={require('@tabler/icons/at.svg')} />
<span>{instance.domain}</span>
</HStack>
)}
{...props}
/>
);
};
export default EditIdentity;

View file

@ -325,7 +325,7 @@ const EditProfile: React.FC = () => {
type='text' type='text'
value={data.nip05} value={data.nip05}
onChange={handleTextChange('nip05')} onChange={handleTextChange('nip05')}
placeholder={intl.formatMessage(messages.nip05Placeholder, { domain: location.host })} placeholder={intl.formatMessage(messages.nip05Placeholder, { domain: instance.domain })}
/> />
</FormGroup> </FormGroup>
)} )}

View file

@ -5,10 +5,10 @@ import Icon from 'soapbox/components/icon';
import { HStack, Stack, Text } from 'soapbox/components/ui'; import { HStack, Stack, Text } from 'soapbox/components/ui';
import { useInstance } from 'soapbox/hooks'; import { useInstance } from 'soapbox/hooks';
import type { Map as ImmutableMap } from 'immutable'; import type { RemoteInstance } from 'soapbox/selectors';
const hasRestrictions = (remoteInstance: ImmutableMap<string, any>): boolean => { const hasRestrictions = (remoteInstance: RemoteInstance): boolean => {
const { accept, reject_deletes, report_removal, ...federation } = remoteInstance.get('federation'); const { accept, reject_deletes, report_removal, ...federation } = remoteInstance.federation;
return !!Object.values(federation).reduce((acc, value) => Boolean(acc || value), false); return !!Object.values(federation).reduce((acc, value) => Boolean(acc || value), false);
}; };
@ -30,7 +30,7 @@ const Restriction: React.FC<IRestriction> = ({ icon, children }) => {
}; };
interface IInstanceRestrictions { interface IInstanceRestrictions {
remoteInstance: ImmutableMap<string, any>; remoteInstance: RemoteInstance;
} }
const InstanceRestrictions: React.FC<IInstanceRestrictions> = ({ remoteInstance }) => { const InstanceRestrictions: React.FC<IInstanceRestrictions> = ({ remoteInstance }) => {
@ -46,7 +46,7 @@ const InstanceRestrictions: React.FC<IInstanceRestrictions> = ({ remoteInstance
followers_only, followers_only,
media_nsfw, media_nsfw,
media_removal, media_removal,
} = remoteInstance.get('federation').toJS(); } = remoteInstance.federation;
const fullMediaRemoval = media_removal && avatar_removal && banner_removal; const fullMediaRemoval = media_removal && avatar_removal && banner_removal;
const partialMediaRemoval = media_removal || avatar_removal || banner_removal; const partialMediaRemoval = media_removal || avatar_removal || banner_removal;
@ -108,10 +108,10 @@ const InstanceRestrictions: React.FC<IInstanceRestrictions> = ({ remoteInstance
const renderContent = () => { const renderContent = () => {
if (!instance || !remoteInstance) return null; if (!instance || !remoteInstance) return null;
const host = remoteInstance.get('host'); const host = remoteInstance.host;
const siteTitle = instance.title; const siteTitle = instance.title;
if (remoteInstance.getIn(['federation', 'reject']) === true) { if (remoteInstance.federation.reject === true) {
return ( return (
<Restriction icon={require('@tabler/icons/shield-x.svg')}> <Restriction icon={require('@tabler/icons/shield-x.svg')}>
<FormattedMessage <FormattedMessage

View file

@ -27,8 +27,8 @@ const RestrictedInstance: React.FC<IRestrictedInstance> = ({ host }) => {
<div> <div>
<a href='#' className='flex items-center gap-1 py-2.5 no-underline' onClick={toggleExpanded}> <a href='#' className='flex items-center gap-1 py-2.5 no-underline' onClick={toggleExpanded}>
<Icon src={expanded ? require('@tabler/icons/caret-down.svg') : require('@tabler/icons/caret-right.svg')} /> <Icon src={expanded ? require('@tabler/icons/caret-down.svg') : require('@tabler/icons/caret-right.svg')} />
<div className={clsx({ 'line-through': remoteInstance.getIn(['federation', 'reject']) })}> <div className={clsx({ 'line-through': remoteInstance.federation.reject })}>
{remoteInstance.get('host')} {remoteInstance.host}
</div> </div>
</a> </a>
<div <div

View file

@ -0,0 +1,80 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { HStack, Input, Select } 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<IRelayEditor> = ({ relays, setRelays }) => {
const handleAddRelay = (): void => {
setRelays([...relays, { url: '' }]);
};
const handleRemoveRelay = (i: number): void => {
const newRelays = [...relays];
newRelays.splice(i, 1);
setRelays(newRelays);
};
return (
<Streamfield
values={relays}
onChange={setRelays}
component={RelayField}
onAddItem={handleAddRelay}
onRemoveItem={handleRemoveRelay}
/>
);
};
interface RelayData {
url: string;
marker?: 'read' | 'write';
}
const RelayField: StreamfieldComponent<RelayData> = ({ value, onChange }) => {
const instance = useInstance();
const handleChange = (key: string): React.ChangeEventHandler<HTMLInputElement> => {
return e => {
onChange({ ...value, [key]: e.currentTarget.value });
};
};
const handleMarkerChange = (e: React.ChangeEvent<HTMLSelectElement>): void => {
onChange({ ...value, marker: (e.currentTarget.value as 'read' | 'write' | '') || undefined });
};
return (
<HStack space={2} grow>
<Input
type='text'
outerClassName='w-full grow'
value={value.url}
onChange={handleChange('url')}
placeholder={instance.nostr?.relay ?? `wss://${instance.domain}/relay`}
/>
<Select className='mt-1' full={false} onChange={handleMarkerChange}>
<option value='' selected={value.marker === undefined}>
<FormattedMessage id='nostr_relays.read_write' defaultMessage='Read & write' />
</option>
<option value='read' selected={value.marker === 'read'}>
<FormattedMessage id='nostr_relays.read_only' defaultMessage='Read-only' />
</option>
<option value='write' selected={value.marker === 'write'}>
<FormattedMessage id='nostr_relays.write_only' defaultMessage='Write-only' />
</option>
</Select>
</HStack>
);
};
export default RelayEditor;
export type { RelayData };

View file

@ -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<RelayData[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(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<void> => {
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 (
<Column label={intl.formatMessage(messages.title)}>
<Form onSubmit={handleSubmit}>
<Stack space={4}>
<RelayEditor relays={relays} setRelays={setRelays} />
<FormActions>
<Button to='/settings' theme='tertiary'>
<FormattedMessage id='common.cancel' defaultMessage='Cancel' />
</Button>
<Button theme='primary' type='submit' disabled={isLoading}>
<FormattedMessage id='edit_profile.save' defaultMessage='Save' />
</Button>
</FormActions>
</Stack>
</Form>
</Column>
);
};
export default NostrRelays;

View file

@ -1,14 +1,17 @@
import { NostrEvent, NostrFilter } from '@soapbox/nspec'; import { NSet, NostrEvent, NostrFilter } from '@soapbox/nspec';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useNostr } from 'soapbox/contexts/nostr-context'; import { useNostr } from 'soapbox/contexts/nostr-context';
import { useForceUpdate } from 'soapbox/hooks/useForceUpdate';
/** Streams events from the relay for the given filters. */ /** Streams events from the relay for the given filters. */
export function useNostrReq(filters: NostrFilter[]): { events: NostrEvent[]; eose: boolean; closed: boolean } { export function useNostrReq(filters: NostrFilter[]): { events: NostrEvent[]; eose: boolean; closed: boolean } {
const { relay } = useNostr(); const { relay } = useNostr();
const [events, setEvents] = useState<NostrEvent[]>([]); const nset = useRef<NSet>(new NSet());
const forceUpdate = useForceUpdate();
const [closed, setClosed] = useState(false); const [closed, setClosed] = useState(false);
const [eose, setEose] = useState(false); const [eose, setEose] = useState(false);
@ -21,7 +24,8 @@ export function useNostrReq(filters: NostrFilter[]): { events: NostrEvent[]; eos
(async () => { (async () => {
for await (const msg of relay.req(value, { signal })) { for await (const msg of relay.req(value, { signal })) {
if (msg[0] === 'EVENT') { if (msg[0] === 'EVENT') {
setEvents((prev) => [msg[2], ...prev]); nset.current.add(msg[2]);
forceUpdate();
} else if (msg[0] === 'EOSE') { } else if (msg[0] === 'EOSE') {
setEose(true); setEose(true);
} else if (msg[0] === 'CLOSED') { } else if (msg[0] === 'CLOSED') {
@ -41,7 +45,7 @@ export function useNostrReq(filters: NostrFilter[]): { events: NostrEvent[]; eos
}, [relay, value]); }, [relay, value]);
return { return {
events, events: [...nset.current],
eose, eose,
closed, closed,
}; };

View file

@ -16,7 +16,7 @@ const PinnedHostsPicker: React.FC<IPinnedHostsPicker> = ({ host: activeHost }) =
return ( return (
<HStack className='mb-4' space={2}> <HStack className='mb-4' space={2}>
{pinnedHosts.map((host: any) => ( {pinnedHosts.map((host) => (
<Button <Button
key={host} key={host}
to={`/timeline/${host}`} to={`/timeline/${host}`}

View file

@ -20,6 +20,8 @@ const messages = defineMessages({
configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' }, configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' },
deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' }, deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' },
editProfile: { id: 'settings.edit_profile', defaultMessage: 'Edit Profile' }, 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' }, exportData: { id: 'column.export_data', defaultMessage: 'Export data' },
importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' }, importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' }, mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' },
@ -65,6 +67,12 @@ const Settings = () => {
<ListItem label={intl.formatMessage(messages.editProfile)} to='/settings/profile'> <ListItem label={intl.formatMessage(messages.editProfile)} to='/settings/profile'>
<span className='max-w-full truncate'>{displayName}</span> <span className='max-w-full truncate'>{displayName}</span>
</ListItem> </ListItem>
{features.nip05 && (
<ListItem label={intl.formatMessage(messages.editIdentity)} to='/settings/identity'>
<span className='max-w-full truncate'>{account?.source?.nostr?.nip05}</span>
</ListItem>
)}
{features.nostr && <ListItem label={intl.formatMessage(messages.editRelays)} to='/settings/relays' />}
</List> </List>
</CardBody> </CardBody>

View file

@ -39,7 +39,7 @@ const InstanceInfoPanel: React.FC<IInstanceInfoPanel> = ({ host }) => {
return ( return (
<Widget <Widget
title={remoteInstance.get('host')} title={remoteInstance.host}
onActionClick={handlePinHost} onActionClick={handlePinHost}
actionIcon={pinned ? require('@tabler/icons/pinned-off.svg') : require('@tabler/icons/pin.svg')} actionIcon={pinned ? require('@tabler/icons/pinned-off.svg') : require('@tabler/icons/pin.svg')}
actionTitle={intl.formatMessage(pinned ? messages.unpinHost : messages.pinHost, { host })} actionTitle={intl.formatMessage(pinned ? messages.unpinHost : messages.pinHost, { host })}

View file

@ -33,7 +33,7 @@ const EditFederationModal: React.FC<IEditFederationModal> = ({ host, onClose })
const [data, setData] = useState<Record<string, any>>({}); const [data, setData] = useState<Record<string, any>>({});
useEffect(() => { useEffect(() => {
setData(remoteInstance.get('federation') as Record<string, any>); setData(remoteInstance.federation);
}, [remoteInstance]); }, [remoteInstance]);
const handleDataChange = (key: string): React.ChangeEventHandler<HTMLInputElement> => { const handleDataChange = (key: string): React.ChangeEventHandler<HTMLInputElement> => {

View file

@ -138,7 +138,9 @@ import {
ExternalLogin, ExternalLogin,
LandingTimeline, LandingTimeline,
BookmarkFolders, BookmarkFolders,
EditIdentity,
Domains, Domains,
NostrRelays,
DraftStatuses, DraftStatuses,
} from './util/async-components'; } from './util/async-components';
import GlobalHotkeys from './util/global-hotkeys'; import GlobalHotkeys from './util/global-hotkeys';
@ -309,11 +311,13 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
<WrappedRoute path='/draft_statuses' page={DefaultPage} component={DraftStatuses} content={children} /> <WrappedRoute path='/draft_statuses' page={DefaultPage} component={DraftStatuses} content={children} />
<WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} /> <WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} />
{features.nip05 && <WrappedRoute path='/settings/identity' page={DefaultPage} component={EditIdentity} content={children} />}
{features.exportData && <WrappedRoute path='/settings/export' page={DefaultPage} component={ExportData} content={children} />} {features.exportData && <WrappedRoute path='/settings/export' page={DefaultPage} component={ExportData} content={children} />}
{features.importData && <WrappedRoute path='/settings/import' page={DefaultPage} component={ImportData} content={children} />} {features.importData && <WrappedRoute path='/settings/import' page={DefaultPage} component={ImportData} content={children} />}
{features.accountAliases && <WrappedRoute path='/settings/aliases' page={DefaultPage} component={Aliases} content={children} />} {features.accountAliases && <WrappedRoute path='/settings/aliases' page={DefaultPage} component={Aliases} content={children} />}
{features.accountMoving && <WrappedRoute path='/settings/migration' page={DefaultPage} component={Migration} content={children} />} {features.accountMoving && <WrappedRoute path='/settings/migration' page={DefaultPage} component={Migration} content={children} />}
{features.backups && <WrappedRoute path='/settings/backups' page={DefaultPage} component={Backups} content={children} />} {features.backups && <WrappedRoute path='/settings/backups' page={DefaultPage} component={Backups} content={children} />}
<WrappedRoute path='/settings/relays' page={DefaultPage} component={NostrRelays} content={children} />
<WrappedRoute path='/settings/email' page={DefaultPage} component={EditEmail} content={children} /> <WrappedRoute path='/settings/email' page={DefaultPage} component={EditEmail} content={children} />
<WrappedRoute path='/settings/password' page={DefaultPage} component={EditPassword} content={children} /> <WrappedRoute path='/settings/password' page={DefaultPage} component={EditPassword} content={children} />
<WrappedRoute path='/settings/account' page={DefaultPage} component={DeleteAccount} content={children} /> <WrappedRoute path='/settings/account' page={DefaultPage} component={DeleteAccount} content={children} />

View file

@ -167,6 +167,8 @@ export const NostrLoginModal = lazy(() => import('soapbox/features/ui/components
export const BookmarkFolders = lazy(() => import('soapbox/features/bookmark-folders')); 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 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 SelectBookmarkFolderModal = lazy(() => import('soapbox/features/ui/components/modals/select-bookmark-folder-modal'));
export const EditIdentity = lazy(() => import('soapbox/features/edit-identity'));
export const Domains = lazy(() => import('soapbox/features/admin/domains')); export const Domains = lazy(() => import('soapbox/features/admin/domains'));
export const EditDomainModal = lazy(() => import('soapbox/features/ui/components/modals/edit-domain-modal')); export const EditDomainModal = lazy(() => import('soapbox/features/ui/components/modals/edit-domain-modal'));
export const NostrRelays = lazy(() => import('soapbox/features/nostr-relays'));
export const DraftStatuses = lazy(() => import('soapbox/features/draft-statuses')); export const DraftStatuses = lazy(() => import('soapbox/features/draft-statuses'));

View file

@ -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;
}

View file

@ -641,6 +641,7 @@
"edit_profile.fields.meta_fields_label": "Profile fields", "edit_profile.fields.meta_fields_label": "Profile fields",
"edit_profile.fields.nip05_label": "Username", "edit_profile.fields.nip05_label": "Username",
"edit_profile.fields.nip05_placeholder": "user@{domain}", "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.stranger_notifications_label": "Block notifications from strangers",
"edit_profile.fields.website_label": "Website", "edit_profile.fields.website_label": "Website",
"edit_profile.fields.website_placeholder": "Display a Link", "edit_profile.fields.website_placeholder": "Display a Link",
@ -1123,6 +1124,10 @@
"new_group_panel.title": "Create Group", "new_group_panel.title": "Create Group",
"nostr_extension.found": "<link>Sign in</link> with browser extension.", "nostr_extension.found": "<link>Sign in</link> with browser extension.",
"nostr_extension.not_found": "Browser extension not found.", "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-add.title": "Import Key",
"nostr_signup.key.title": "You need a key to continue", "nostr_signup.key.title": "You need a key to continue",
"nostr_signup.keygen.title": "Your new key", "nostr_signup.keygen.title": "Your new key",
@ -1385,6 +1390,7 @@
"settings.change_password": "Change Password", "settings.change_password": "Change Password",
"settings.configure_mfa": "Configure MFA", "settings.configure_mfa": "Configure MFA",
"settings.delete_account": "Delete Account", "settings.delete_account": "Delete Account",
"settings.edit_identity": "Identity",
"settings.edit_profile": "Edit Profile", "settings.edit_profile": "Edit Profile",
"settings.messages.label": "Allow users to start a new chat with you", "settings.messages.label": "Allow users to start a new chat with you",
"settings.mutes": "Mutes", "settings.mutes": "Mutes",

View file

@ -2,6 +2,7 @@ import {
Map as ImmutableMap, Map as ImmutableMap,
List as ImmutableList, List as ImmutableList,
OrderedSet as ImmutableOrderedSet, OrderedSet as ImmutableOrderedSet,
Record as ImmutableRecord,
fromJS, fromJS,
} from 'immutable'; } from 'immutable';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
@ -305,7 +306,7 @@ const getRemoteInstanceFavicon = (state: RootState, host: string) => {
return account?.pleroma?.favicon; return account?.pleroma?.favicon;
}; };
type HostFederation = { export type HostFederation = {
[key in keyof MRFSimple]: boolean; [key in keyof MRFSimple]: boolean;
}; };
@ -328,19 +329,25 @@ export const makeGetHosts = () => {
}); });
}; };
export const makeGetRemoteInstance = () => { export const RemoteInstanceRecord = ImmutableRecord({
return createSelector([ host: '',
favicon: null as string | null,
federation: null as unknown as HostFederation,
});
export type RemoteInstance = ReturnType<typeof RemoteInstanceRecord>;
export const makeGetRemoteInstance = () =>
createSelector([
(_state: RootState, host: string) => host, (_state: RootState, host: string) => host,
getRemoteInstanceFavicon, getRemoteInstanceFavicon,
getRemoteInstanceFederation, getRemoteInstanceFederation,
], (host, favicon, federation) => { ], (host, favicon, federation) =>
return ImmutableMap({ RemoteInstanceRecord({
host, host,
favicon, favicon,
federation, federation,
}); }));
});
};
type ColumnQuery = { type: string; prefix?: string }; type ColumnQuery = { type: string; prefix?: string };

View file

@ -757,6 +757,9 @@ const getInstanceFeatures = (instance: Instance) => {
*/ */
nip05: v.software === DITTO, nip05: v.software === DITTO,
/** Has a Nostr relay. */
nostr: !!instance.nostr?.relay,
/** /**
* Ability to sign Nostr events over websocket. * Ability to sign Nostr events over websocket.
* @see GET /api/v1/streaming?stream=nostr * @see GET /api/v1/streaming?stream=nostr

248
yarn.lock
View file

@ -1685,160 +1685,160 @@
"@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@jridgewell/sourcemap-codec" "^1.4.14"
"@lexical/clipboard@0.13.1", "@lexical/clipboard@^0.13.1": "@lexical/clipboard@0.14.2", "@lexical/clipboard@^0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.13.1.tgz#ca132306129974ea2c9e51d6a8637f8fcffcdb3d" resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.14.2.tgz#4638acdc80816500e87096646a98f24a34b94fa9"
integrity sha512-gMSbVeqb7S+XAi/EMMlwl+FCurLPugN2jAXcp5k5ZaUd7be8B+iupbYdoKkjt4qBhxmvmfe9k46GoC0QOPl/nw== integrity sha512-WevZZ+VPpkvcgZZXxjHY2lc3+2kw+UA6q19MWdZA2y4NVQyeDmjxQp5uxdUWGTKTBzt22vlzeQ2jbtKz4wY3/g==
dependencies: dependencies:
"@lexical/html" "0.13.1" "@lexical/html" "0.14.2"
"@lexical/list" "0.13.1" "@lexical/list" "0.14.2"
"@lexical/selection" "0.13.1" "@lexical/selection" "0.14.2"
"@lexical/utils" "0.13.1" "@lexical/utils" "0.14.2"
"@lexical/code@0.13.1": "@lexical/code@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.13.1.tgz#e13688390582a4b63a639daff1f16bcb82aa854d" resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.14.2.tgz#e23443275b342e245684cc4e1889130edd746cbc"
integrity sha512-QK77r3QgEtJy96ahYXNgpve8EY64BQgBSnPDOuqVrLdl92nPzjqzlsko2OZldlrt7gjXcfl9nqfhZ/CAhStfOg== integrity sha512-XByweRm8flv/2nwwLbnZwskl2y6jWuDFuKB/8ywvSenMsGyr7OSg5ALJrsFuX5KUo9mEGGpaiM8UVeMoeaqMgg==
dependencies: dependencies:
"@lexical/utils" "0.13.1" "@lexical/utils" "0.14.2"
prismjs "^1.27.0" prismjs "^1.27.0"
"@lexical/dragon@0.13.1": "@lexical/dragon@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.13.1.tgz#32ba02bff4d8f02a6317d874671ee0b0a2dcdc53" resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.14.2.tgz#493af45501550227b0274ff8b7307a2d9ba0c38d"
integrity sha512-aNlqfif4//jW7gOxbBgdrbDovU6m3EwQrUw+Y/vqRkY+sWmloyAUeNwCPH1QP3Q5cvfolzOeN5igfBljsFr+1g== integrity sha512-yzYJ3HD/goZZW5+ckYGCidtQ5zwrT08yDPhNgFUh9eni2ePQ9b56x2Bko01urocIkEqA0d6VEtfoO+hAOHiq6Q==
"@lexical/hashtag@0.13.1", "@lexical/hashtag@^0.13.1": "@lexical/hashtag@0.14.2", "@lexical/hashtag@^0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.13.1.tgz#eb273c199a0115ec0f0191c2449e97f512360f2e" resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.14.2.tgz#7ae794480079c702e707319e7b588aec39b2b060"
integrity sha512-Dl0dUG4ZXNjYYuAUR0GMGpLGsA+cps2/ln3xEmy28bZR0sKkjXugsu2QOIxZjYIPBewDrXzPcvK8md45cMYoSg== integrity sha512-H2z71iGX3n0ZB45y5uoI7aPwVjClo01df480IrTecKxr4aAKbK5LhFQWOphiUXBiv4O1sn+Xa88f8IpbfqQ5Vg==
dependencies: dependencies:
"@lexical/utils" "0.13.1" "@lexical/utils" "0.14.2"
"@lexical/history@0.13.1": "@lexical/history@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.13.1.tgz#3bb54716dc69779d3b35894bd72637a7fc2ed284" resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.14.2.tgz#740a6dffd0c5250ada12ce13f514d0c3f7df960b"
integrity sha512-cZXt30MalEEiRaflE9tHeGYnwT1xSDjXLsf9M409DSU9POJyZ1fsULJrG1tWv2uFQOhwal33rve9+MatUlITrg== integrity sha512-k0gNIKCDeyIWEHIJKHZFfGVEN4yPID8tfuR4/R1M12WUwnga/X3regxPSMV8LE1SWbCz6iiQuaeZozxOJ3AlRw==
dependencies: dependencies:
"@lexical/utils" "0.13.1" "@lexical/utils" "0.14.2"
"@lexical/html@0.13.1": "@lexical/html@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.13.1.tgz#e56035d0c6528ffb932390e0d3d357c82f69253a" resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.14.2.tgz#8b8f0d81d0afdea9865ef9fba814705daabbbf23"
integrity sha512-XkZrnCSHIUavtpMol6aG8YsJ5KqC9hMxEhAENf3HTGi3ocysCByyXOyt1EhEYpjJvgDG4wRqt25xGDbLjj1/sA== integrity sha512-5uL0wSfS9H5/HNeCM4QaJMekoL1w4D81361RlC2ppKt1diSzLiWOITX1qElaTcnDJBGez5mv1ZNiRTutYOPV4Q==
dependencies: dependencies:
"@lexical/selection" "0.13.1" "@lexical/selection" "0.14.2"
"@lexical/utils" "0.13.1" "@lexical/utils" "0.14.2"
"@lexical/link@0.13.1", "@lexical/link@^0.13.1": "@lexical/link@0.14.2", "@lexical/link@^0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.13.1.tgz#f1c4c12c828c0251e5d7fb4fb336f2d62380fc57" resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.14.2.tgz#78c220a81475505b17fb8fa6c5baff08a3ed3b5b"
integrity sha512-7E3B2juL2UoMj2n+CiyFZ7tlpsdViAoIE7MpegXwfe/VQ66wFwk/VxGTa/69ng2EoF7E0kh+SldvGQDrWAWb1g== integrity sha512-XD4VdxtBm9Yx5vk2hDEDKY1BjgNVdfmxQHo6Y/kyImAHhGRiBWa6V1+l55qfgcjPW3tN2QY/gSKDCPQGk7vKJw==
dependencies: dependencies:
"@lexical/utils" "0.13.1" "@lexical/utils" "0.14.2"
"@lexical/list@0.13.1": "@lexical/list@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.13.1.tgz#461cb989157bdf4a43eaa8596fdb09df60d114ee" resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.14.2.tgz#b1d70e95e9b6a22f22b47f11ed7b667b1eeacdd2"
integrity sha512-6U1pmNZcKLuOWiWRML8Raf9zSEuUCMlsOye82niyF6I0rpPgYo5UFghAAbGISDsyqzM1B2L4BgJ6XrCk/dJptg== integrity sha512-74MVHcYtTC5Plj+GGRV08uk9qbI1AaKc37NGLe3T08aVBqzqxXl1qZK9BhrM2mqTVXB98ZnOXkBk+07vke+b0Q==
dependencies: dependencies:
"@lexical/utils" "0.13.1" "@lexical/utils" "0.14.2"
"@lexical/mark@0.13.1": "@lexical/mark@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.13.1.tgz#084bb49a8bc1c5c5a4ed5c5d4a20c98ea85ec8b1" resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.14.2.tgz#0f264d7f287c380e99a118eeba84a4bb94abc403"
integrity sha512-dW27PW8wWDOKFqXTBUuUfV+umU0KfwvXGkPUAxRJrvwUWk5RKaS48LhgbNlQ5BfT84Q8dSiQzvbaa6T40t9a3A== integrity sha512-8G1p2tuUkymWXvWgUUShZp5AgYIODUZrBYDpGsCBNkXuSdGagOirS5LhKeiT/68BnrPzGrlnCdmomnI/kMxh6w==
dependencies: dependencies:
"@lexical/utils" "0.13.1" "@lexical/utils" "0.14.2"
"@lexical/markdown@0.13.1": "@lexical/markdown@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.13.1.tgz#1fd2efcacff4ce733682a8161a3f3d78dba37503" resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.14.2.tgz#26ae02482708632bc2e0e4a662c39a443f8e3bdc"
integrity sha512-6tbdme2h5Zy/M88loVQVH5G0Nt7VMR9UUkyiSaicyBRDOU2OHacaXEp+KSS/XuF+d7TA+v/SzyDq8HS77cO1wA== integrity sha512-U5P8ceEhiQqEKyy3dx4ldVBdoajquVndrZ4TvS6HJs8jeqOH49sLMvbKtNpXPL1plGvvwAjutQzEUdY+ifpGgw==
dependencies: dependencies:
"@lexical/code" "0.13.1" "@lexical/code" "0.14.2"
"@lexical/link" "0.13.1" "@lexical/link" "0.14.2"
"@lexical/list" "0.13.1" "@lexical/list" "0.14.2"
"@lexical/rich-text" "0.13.1" "@lexical/rich-text" "0.14.2"
"@lexical/text" "0.13.1" "@lexical/text" "0.14.2"
"@lexical/utils" "0.13.1" "@lexical/utils" "0.14.2"
"@lexical/offset@0.13.1": "@lexical/offset@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.13.1.tgz#f37417822aef3dc81580d4abb96e43ba9d547225" resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.14.2.tgz#7cbade3817b55e305013a61698465df9f4e8b3ab"
integrity sha512-j/RZcztJ7dyTrfA2+C3yXDzWDXV+XmMpD5BYeQCEApaHvlo20PHt1BISk7RcrnQW8PdzGvpKblRWf//c08LS9w== integrity sha512-bv6M+HITGNDuXvIELB1NLobRKoxtP1JPG3zDhKGPnLyjzFeHE5FZ6QbGc9y8ltrhUafVmxgzW/Es0IJXTsGf7Q==
"@lexical/overflow@0.13.1": "@lexical/overflow@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.13.1.tgz#42c036dc3ad3eb929fda5aa0a00a725b74f72669" resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.14.2.tgz#63482f6941b0f21bf79639e36fd5711052e84adf"
integrity sha512-Uw34j+qG2UJRCIR+bykfFMduFk7Pc4r/kNt8N1rjxGuGXAsreTVch1iOhu7Ev6tJgkURsduKuaJCAi7iHnKl7g== integrity sha512-2eHirK9GmGr7juMjeF54fZ8xPwdMMzni+FXC4NVMDWzzM3yjK3BCBK6AUJsU3o4Y4bV3mUvZp55x9Ys5lX3zOw==
"@lexical/plain-text@0.13.1": "@lexical/plain-text@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.13.1.tgz#e7e713029443c30facce27b34836bf604cf92c0f" resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.14.2.tgz#498306c83bbd5be24f6c6935670e77247763d866"
integrity sha512-4j5KAsMKUvJ8LhVDSS4zczbYXzdfmgYSAVhmqpSnJtud425Nk0TAfpUBLFoivxZB7KMoT1LGWQZvd47IvJPvtA== integrity sha512-vHvJ+Dy+SpzZ0hVN3r+DONHoSn0WzuK6htwsV0IyaMyEeypxnFs6FB/znyWmoU+X2EAhDCu5O9npLDWmrJjCdQ==
"@lexical/react@^0.13.1": "@lexical/react@^0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.13.1.tgz#6c35bf43e24560d2ca3aa2c6ff607ef37de87bac" resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.14.2.tgz#2a6b812f0e07cba05b02ff2b8bdfb9b4afc05873"
integrity sha512-Sy6EL230KAb0RZsZf1dZrRrc3+rvCDQWltcd8C/cqBUYlxsLYCW9s4f3RB2werngD/PtLYbBB48SYXNkIALITA== integrity sha512-HCsZv5yExA/8h7Ul6y5NcqUAGctL79A7qSz7YSlrwfw8EYyUHXW5LzeoFTWSl12SKs600PM+I8jLGXu72N6kfQ==
dependencies: dependencies:
"@lexical/clipboard" "0.13.1" "@lexical/clipboard" "0.14.2"
"@lexical/code" "0.13.1" "@lexical/code" "0.14.2"
"@lexical/dragon" "0.13.1" "@lexical/dragon" "0.14.2"
"@lexical/hashtag" "0.13.1" "@lexical/hashtag" "0.14.2"
"@lexical/history" "0.13.1" "@lexical/history" "0.14.2"
"@lexical/link" "0.13.1" "@lexical/link" "0.14.2"
"@lexical/list" "0.13.1" "@lexical/list" "0.14.2"
"@lexical/mark" "0.13.1" "@lexical/mark" "0.14.2"
"@lexical/markdown" "0.13.1" "@lexical/markdown" "0.14.2"
"@lexical/overflow" "0.13.1" "@lexical/overflow" "0.14.2"
"@lexical/plain-text" "0.13.1" "@lexical/plain-text" "0.14.2"
"@lexical/rich-text" "0.13.1" "@lexical/rich-text" "0.14.2"
"@lexical/selection" "0.13.1" "@lexical/selection" "0.14.2"
"@lexical/table" "0.13.1" "@lexical/table" "0.14.2"
"@lexical/text" "0.13.1" "@lexical/text" "0.14.2"
"@lexical/utils" "0.13.1" "@lexical/utils" "0.14.2"
"@lexical/yjs" "0.13.1" "@lexical/yjs" "0.14.2"
react-error-boundary "^3.1.4" react-error-boundary "^3.1.4"
"@lexical/rich-text@0.13.1": "@lexical/rich-text@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.13.1.tgz#8251e81a3985a4d76bef027cf6c0dc90c661e4ec" resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.14.2.tgz#d2ef208b5f5b0fb4021c61a00187ba8da016ce69"
integrity sha512-HliB9Ync06mv9DBg/5j0lIsTJp+exLHlaLJe+n8Zq1QNTzZzu2LsIT/Crquk50In7K/cjtlaQ/d5RB0LkjMHYg== integrity sha512-KXxeVfzHw4volw5Tm/TXGCDH+OCvFm1QU17LK3ToAulN2M+j0745yxpudtRfcl96fIbB9THY2toAZpJ9YbMTNQ==
"@lexical/selection@0.13.1", "@lexical/selection@^0.13.1": "@lexical/selection@0.14.2", "@lexical/selection@^0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.13.1.tgz#466d7cd0ee1b04680bd949112f1f5cb6a6618efa" resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.14.2.tgz#39d6c0db233790eec81125653503e20deadc13a9"
integrity sha512-Kt9eSwjxPznj7yzIYipu9yYEgmRJhHiq3DNxHRxInYcZJWWNNHum2xKyxwwcN8QYBBzgfPegfM/geqQEJSV1lQ== integrity sha512-M122XXGEiBgaxEhL63d+su0pPri67/GlFIwGC/j3D0TN4Giyt/j0ToHhqvlIF6TfuXlBusIYbSuJ19ny12lCEg==
"@lexical/table@0.13.1": "@lexical/table@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.13.1.tgz#814d3b8a2afb821aff151c92cce831809f9d67a1" resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.14.2.tgz#a28e0e24dfc8c43ac4ccf53dd33c1292969740d6"
integrity sha512-VQzgkfkEmnvn6C64O/kvl0HI3bFoBh3WA/U67ALw+DS11Mb5CKjbt0Gzm/258/reIxNMpshjjicpWMv9Miwauw== integrity sha512-iwsZ5AqkM7RGyU38daK0XgpC8DG0TlEqEYsXhOLjCpAERY/+bgfdjxP8YWtUV5eIgHX0yY7FkqCUZUJSEcbUeA==
dependencies: dependencies:
"@lexical/utils" "0.13.1" "@lexical/utils" "0.14.2"
"@lexical/text@0.13.1": "@lexical/text@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.13.1.tgz#12104d42da7a707a19853679f3a88e8ed6ce8084" resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.14.2.tgz#5a6fdd78ac3ec62baa23bbb3466046b7b18f2dfb"
integrity sha512-NYy3TZKt3qzReDwN2Rr5RxyFlg84JjXP2JQGMrXSSN7wYe73ysQIU6PqdVrz4iZkP+w34F3pl55dJ24ei3An9w== integrity sha512-5N1Bwr75EGdP/4izQRzoxyKpozy+0YixGJOo3OoldlX9/j+Sf5XiCFj3Ifgfl5dygZ0oITQk4duHpzjPHez59Q==
"@lexical/utils@0.13.1", "@lexical/utils@^0.13.1": "@lexical/utils@0.14.2", "@lexical/utils@^0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.13.1.tgz#f2a72f71c859933781294830b38b25b5b33122a9" resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.14.2.tgz#7ecb2e16f2a395c96be6de0128bcb7de54589872"
integrity sha512-AtQQKzYymkbOaQxaBXjRBS8IPxF9zWQnqwHTUTrJqJ4hX71aIQd/thqZbfQETAFJfC8pNBZw5zpxN6yPHk23dQ== integrity sha512-IGknsaSyQbBJYKJJrrjNPaZuQPsJmFqGrCmNR6DcQNenWrFnmAliQPFA7HbszwRSxOFTo/BCAsIgXRQob6RjOQ==
dependencies: dependencies:
"@lexical/list" "0.13.1" "@lexical/list" "0.14.2"
"@lexical/selection" "0.13.1" "@lexical/selection" "0.14.2"
"@lexical/table" "0.13.1" "@lexical/table" "0.14.2"
"@lexical/yjs@0.13.1": "@lexical/yjs@0.14.2":
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.13.1.tgz#2a71ae3c4b3cc5c660bbe66d537eb0cbf3c7c1b6" resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.14.2.tgz#30226dfb51a44195e7dfb1bf064198f80c629c07"
integrity sha512-4GbqQM+PwNTV59AZoNrfTe/0rLjs+cX6Y6yAdZSRPBwr5L3JzYeU1TTcFCVQTtsE7KF8ddVP8sD7w9pi8rOWLA== integrity sha512-0AKwQYRx7KvAKTdizoAXd9XZhkTO6l3LmHRvsu70YAHVZUEb9tVQBM7lp/dtBBQF0SII/7uKwrluX6Vi/apWYA==
dependencies: dependencies:
"@lexical/offset" "0.13.1" "@lexical/offset" "0.14.2"
"@mdn/browser-compat-data@^5.2.34", "@mdn/browser-compat-data@^5.3.13": "@mdn/browser-compat-data@^5.2.34", "@mdn/browser-compat-data@^5.3.13":
version "5.3.16" version "5.3.16"
@ -6183,10 +6183,10 @@ levn@^0.4.1:
prelude-ls "^1.2.1" prelude-ls "^1.2.1"
type-check "~0.4.0" type-check "~0.4.0"
lexical@^0.13.1: lexical@^0.14.2:
version "0.13.1" version "0.14.2"
resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.13.1.tgz#0abffe9bc05a7a9da8a6128ea478bf08c11654db" resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.14.2.tgz#592e0e16ef415a4423be7ff65ac995fc4da40c90"
integrity sha512-jaqRYzVEfBKbX4FwYpd/g+MyOjRaraAel0iQsTrwvx3hyN0bswUZuzb6H6nGlFSjcdrc77wKpyKwoWj4aUd+Bw== integrity sha512-Uxe0jD2T4XY/+WKiVgnV6OH/GmsF1I0YStcSuMR3Alfhnv5MEYuCa482zo+S5zOPjB1x9j/b+TOLtZEMArwELw==
li@^1.3.0: li@^1.3.0:
version "1.3.0" version "1.3.0"