Merge pull request #1 from mkljczk/denostrify

Remove Nostr-related stuff
This commit is contained in:
Marcin Mikołajczak 2024-04-29 18:45:03 +02:00 committed by GitHub
commit aedf097ec0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 88 additions and 1665 deletions

View file

@ -73,8 +73,6 @@
"@sentry/browser": "^7.74.1",
"@sentry/react": "^7.74.1",
"@soapbox.pub/wasmboy": "^0.8.0",
"@soapbox/nspec": "npm:@jsr/soapbox__nspec",
"@soapbox/weblock": "npm:@jsr/soapbox__weblock",
"@tabler/icons": "^3.1.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.7",
@ -136,8 +134,6 @@
"localforage": "^1.10.0",
"lodash": "^4.7.11",
"mini-css-extract-plugin": "^2.6.0",
"nostr-machina": "^0.1.0",
"nostr-tools": "^2.3.0",
"path-browserify": "^1.0.1",
"postcss": "^8.4.29",
"process": "^0.11.10",

View file

@ -79,10 +79,6 @@ const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
const ZAP_REQUEST = 'ZAP_REQUEST';
const ZAP_SUCCESS = 'ZAP_SUCCESS';
const ZAP_FAIL = 'ZAP_FAIL';
const messages = defineMessages({
bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' },
bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' },
@ -313,38 +309,6 @@ const undislikeFail = (status: StatusEntity, error: unknown) => ({
skipLoading: true,
});
const zap = (status: StatusEntity, amount: number) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch(zapRequest(status));
api(getState).post(`/api/v1/statuses/${status.id}/zap`, { amount }).then(function(response) {
dispatch(zapSuccess(status));
}).catch(function(error) {
dispatch(zapFail(status, error));
});
};
const zapRequest = (status: StatusEntity) => ({
type: ZAP_REQUEST,
status: status,
skipLoading: true,
});
const zapSuccess = (status: StatusEntity) => ({
type: ZAP_SUCCESS,
status: status,
skipLoading: true,
});
const zapFail = (status: StatusEntity, error: unknown) => ({
type: ZAP_FAIL,
status: status,
error: error,
skipLoading: true,
});
const bookmark = (status: StatusEntity, folderId?: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
@ -775,8 +739,6 @@ export {
FAVOURITES_EXPAND_FAIL,
REBLOGS_EXPAND_SUCCESS,
REBLOGS_EXPAND_FAIL,
ZAP_REQUEST,
ZAP_FAIL,
reblog,
unreblog,
toggleReblog,
@ -844,5 +806,4 @@ export {
remoteInteractionRequest,
remoteInteractionSuccess,
remoteInteractionFail,
zap,
};

View file

@ -1,26 +0,0 @@
import { nip19 } from 'nostr-tools';
import { type AppDispatch } from 'soapbox/store';
import { verifyCredentials } from './auth';
/** Log in with a Nostr pubkey. */
function logInNostr(pubkey: string) {
return (dispatch: AppDispatch) => {
const npub = nip19.npubEncode(pubkey);
return dispatch(verifyCredentials(npub));
};
}
/** Log in with a Nostr extension. */
function nostrExtensionLogIn() {
return async (dispatch: AppDispatch) => {
if (!window.nostr) {
throw new Error('No Nostr signer available');
}
const pubkey = await window.nostr.getPublicKey();
return dispatch(logInNostr(pubkey));
};
}
export { logInNostr, nostrExtensionLogIn };

View file

@ -1,148 +0,0 @@
import { type NostrEvent } from '@soapbox/nspec';
import { useEffect } from 'react';
import { useNostr } from 'soapbox/contexts/nostr-context';
import { connectRequestSchema, nwcRequestSchema } from 'soapbox/schemas/nostr';
import { jsonSchema } from 'soapbox/schemas/utils';
/** NIP-46 [response](https://github.com/nostr-protocol/nips/blob/master/46.md#response-events-kind24133) content. */
interface NostrConnectResponse {
/** Request ID that this response is for. */
id: string;
/** Result of the call (this can be either a string or a JSON stringified object) */
result: string;
/** Error in string form, if any. Its presence indicates an error with the request. */
error?: string;
}
function useSignerStream() {
const { relay, pubkey, signer } = useNostr();
async function sendConnect(response: NostrConnectResponse) {
if (!relay || !pubkey || !signer) return;
const event = await signer.signEvent({
kind: 24133,
content: await signer.nip04!.encrypt(pubkey, JSON.stringify(response)),
tags: [['p', pubkey]],
created_at: Math.floor(Date.now() / 1000),
});
relay.event(event);
}
async function handleConnectEvent(event: NostrEvent) {
if (!relay || !pubkey || !signer) return;
const decrypted = await signer.nip04!.decrypt(pubkey, event.content);
const reqMsg = jsonSchema.pipe(connectRequestSchema).safeParse(decrypted);
if (!reqMsg.success) {
console.warn(decrypted);
console.warn(reqMsg.error);
return;
}
const request = reqMsg.data;
switch (request.method) {
case 'connect':
return sendConnect({
id: request.id,
result: 'ack',
});
case 'sign_event':
return sendConnect({
id: request.id,
result: JSON.stringify(await signer.signEvent(JSON.parse(request.params[0]))),
});
case 'ping':
return sendConnect({
id: request.id,
result: 'pong',
});
case 'get_relays':
return sendConnect({
id: request.id,
result: JSON.stringify(await signer.getRelays?.() ?? []),
});
case 'get_public_key':
return sendConnect({
id: request.id,
result: await signer.getPublicKey(),
});
case 'nip04_encrypt':
return sendConnect({
id: request.id,
result: await signer.nip04!.encrypt(request.params[0], request.params[1]),
});
case 'nip04_decrypt':
return sendConnect({
id: request.id,
result: await signer.nip04!.decrypt(request.params[0], request.params[1]),
});
case 'nip44_encrypt':
return sendConnect({
id: request.id,
result: await signer.nip44!.encrypt(request.params[0], request.params[1]),
});
case 'nip44_decrypt':
return sendConnect({
id: request.id,
result: await signer.nip44!.decrypt(request.params[0], request.params[1]),
});
default:
return sendConnect({
id: request.id,
result: '',
error: `Unrecognized method: ${request.method}`,
});
}
}
async function handleWalletEvent(event: NostrEvent) {
if (!relay || !pubkey || !signer) return;
const decrypted = await signer.nip04!.decrypt(pubkey, event.content);
const reqMsg = jsonSchema.pipe(nwcRequestSchema).safeParse(decrypted);
if (!reqMsg.success) {
console.warn(decrypted);
console.warn(reqMsg.error);
return;
}
await window.webln?.enable();
await window.webln?.sendPayment(reqMsg.data.params.invoice);
}
async function handleEvent(event: NostrEvent) {
switch (event.kind) {
case 24133:
await handleConnectEvent(event);
break;
case 23194:
await handleWalletEvent(event);
break;
}
}
useEffect(() => {
if (!relay || !pubkey) return;
const controller = new AbortController();
const signal = controller.signal;
(async() => {
for await (const msg of relay.req([{ kinds: [24133, 23194], authors: [pubkey], limit: 0 }], { signal })) {
if (msg[0] === 'EVENT') handleEvent(msg[2]);
}
})();
return () => {
controller.abort();
};
}, [relay, pubkey, signer]);
}
export { useSignerStream };

View file

@ -62,7 +62,6 @@ const getAuthBaseURL = createSelector([
export const baseClient = (
accessToken?: string | null,
baseURL: string = '',
nostrSign = false,
): AxiosInstance => {
const headers: Record<string, string> = {};
@ -70,10 +69,6 @@ export const baseClient = (
headers.Authorization = `Bearer ${accessToken}`;
}
if (nostrSign) {
headers['X-Nostr-Sign'] = 'true';
}
return axios.create({
// When BACKEND_URL is set, always use it.
baseURL: isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : baseURL,
@ -105,11 +100,7 @@ export default (getState: () => RootState, authType: string = 'user'): AxiosInst
const me = state.me;
const baseURL = me ? getAuthBaseURL(state, me) : '';
const relayUrl = state.getIn(['instance', 'nostr', 'relay']) as string | undefined;
const pubkey = state.getIn(['instance', 'nostr', 'pubkey']) as string | undefined;
const nostrSign = Boolean(relayUrl && pubkey);
return baseClient(accessToken, baseURL, nostrSign);
return baseClient(accessToken, baseURL);
};
// The Jest mock exports these, so they're needed for TypeScript.

View file

@ -1,8 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { isPubkey } from 'soapbox/utils/nostr';
import { Tooltip } from './ui';
import type { Mention as MentionEntity } from 'soapbox/schemas';
@ -29,7 +27,7 @@ const Mention: React.FC<IMention> = ({ mention: { acct, username }, disabled })
onClick={handleClick}
dir='ltr'
>
@{isPubkey(username) ? username.slice(0, 8) : username}
@{username}
</Link>
</Tooltip>
);

View file

@ -6,7 +6,7 @@ import { blockAccount } from 'soapbox/actions/accounts';
import { launchChat } from 'soapbox/actions/chats';
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'soapbox/actions/compose';
import { editEvent } from 'soapbox/actions/events';
import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog, zap } from 'soapbox/actions/interactions';
import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'soapbox/actions/interactions';
import { openModal } from 'soapbox/actions/modals';
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
import { initMuteModal } from 'soapbox/actions/mutes';
@ -95,7 +95,6 @@ const messages = defineMessages({
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute Conversation' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
zap: { id: 'status.zap', defaultMessage: 'Zap' },
});
interface IStatusActionBar {
@ -180,14 +179,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
}
};
const handleZapClick: React.EventHandler<React.MouseEvent> = (e) => {
if (me) {
dispatch(zap(status, 1337));
} else {
onOpenUnauthorizedModal('ZAP');
}
};
const handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => {
dispatch(toggleBookmark(status));
};
@ -647,7 +638,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
}
const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group');
const acceptsZaps = status.account.ditto.accepts_zaps === true;
const spacing: {
[key: string]: React.ComponentProps<typeof HStack>['space'];
@ -735,19 +725,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
/>
)}
{(acceptsZaps && window.webln) && (
<StatusActionButton
title={intl.formatMessage(messages.zap)}
icon={require('@tabler/icons/outline/bolt.svg')}
color='accent'
filled
onClick={handleZapClick}
active={status.zapped}
text={withLabels ? intl.formatMessage(messages.zap) : undefined}
theme={statusActionButtonTheme}
/>
)}
{canShare && (
<StatusActionButton
title={intl.formatMessage(messages.share)}

View file

@ -6,7 +6,6 @@ import { openModal } from 'soapbox/actions/modals';
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
import HoverStatusWrapper from 'soapbox/components/hover-status-wrapper';
import { useAppDispatch } from 'soapbox/hooks';
import { isPubkey } from 'soapbox/utils/nostr';
import type { Status } from 'soapbox/types/entities';
@ -57,7 +56,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
className='inline-block max-w-[200px] truncate align-bottom text-primary-600 no-underline [direction:ltr] hover:text-primary-700 hover:underline dark:text-accent-blue dark:hover:text-accent-blue'
onClick={(e) => e.stopPropagation()}
>
@{isPubkey(account.username) ? account.username.slice(0, 8) : account.username}
@{account.username}
</Link>
);

View file

@ -1,57 +0,0 @@
import { NRelay, NRelay1, NostrSigner } from '@soapbox/nspec';
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
import { NKeys } from 'soapbox/features/nostr/keys';
import { useOwnAccount } from 'soapbox/hooks';
import { useInstance } from 'soapbox/hooks/useInstance';
interface NostrContextType {
relay?: NRelay;
pubkey?: string;
signer?: NostrSigner;
}
const NostrContext = createContext<NostrContextType | undefined>(undefined);
interface NostrProviderProps {
children: React.ReactNode;
}
export const NostrProvider: React.FC<NostrProviderProps> = ({ children }) => {
const instance = useInstance();
const [relay, setRelay] = useState<NRelay1>();
const { account } = useOwnAccount();
const url = instance.nostr?.relay;
const pubkey = instance.nostr?.pubkey;
const accountPubkey = account?.nostr.pubkey;
const signer = useMemo(
() => (accountPubkey ? NKeys.get(accountPubkey) : undefined) ?? window.nostr,
[accountPubkey],
);
useEffect(() => {
if (url) {
setRelay(new NRelay1(url));
}
return () => {
relay?.close();
};
}, [url]);
return (
<NostrContext.Provider value={{ relay, pubkey, signer }}>
{children}
</NostrContext.Provider>
);
};
export const useNostr = () => {
const context = useContext(NostrContext);
if (context === undefined) {
throw new Error('useNostr must be used within a NostrProvider');
}
return context;
};

View file

@ -4,9 +4,9 @@ import { Redirect } from 'react-router-dom';
import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
import { fetchInstance } from 'soapbox/actions/instance';
import { closeModal, openModal } from 'soapbox/actions/modals';
import { closeModal } from 'soapbox/actions/modals';
import { BigCard } from 'soapbox/components/big-card';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { getRedirectUrl } from 'soapbox/utils/redirect';
import { isStandalone } from 'soapbox/utils/state';
@ -21,7 +21,6 @@ const LoginPage = () => {
const me = useAppSelector((state) => state.me);
const standalone = useAppSelector((state) => isStandalone(state));
const { nostrSignup } = useFeatures();
const token = new URLSearchParams(window.location.search).get('token');
@ -63,11 +62,6 @@ const LoginPage = () => {
event.preventDefault();
};
if (nostrSignup) {
setTimeout(() => dispatch(openModal('NOSTR_LOGIN')), 100);
return <Redirect to='/' />;
}
if (standalone) return <Redirect to='/login/external' />;
if (shouldRedirect) {

View file

@ -270,53 +270,46 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
</FormGroup>
)}
<Input
type='email'
name='email'
placeholder={intl.formatMessage(messages.email)}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
onChange={onInputChange}
value={params.get('email', '')}
required
/>
{!features.nostrSignup && (
<Input
type='password'
name='password'
placeholder={intl.formatMessage(messages.password)}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
onChange={onPasswordChange}
value={params.get('password', '')}
required
/>
<FormGroup
errors={passwordMismatch ? [intl.formatMessage(messages.passwordMismatch)] : undefined}
>
<Input
type='email'
name='email'
placeholder={intl.formatMessage(messages.email)}
type='password'
name='password_confirmation'
placeholder={intl.formatMessage(messages.confirm)}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
onChange={onInputChange}
value={params.get('email', '')}
onChange={onPasswordConfirmChange}
onBlur={onPasswordConfirmBlur}
value={passwordConfirmation}
required
/>
)}
{!features.nostrSignup && (
<>
<Input
type='password'
name='password'
placeholder={intl.formatMessage(messages.password)}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
onChange={onPasswordChange}
value={params.get('password', '')}
required
/>
<FormGroup
errors={passwordMismatch ? [intl.formatMessage(messages.passwordMismatch)] : undefined}
>
<Input
type='password'
name='password_confirmation'
placeholder={intl.formatMessage(messages.confirm)}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
onChange={onPasswordConfirmChange}
onBlur={onPasswordConfirmBlur}
value={passwordConfirmation}
required
/>
</FormGroup>
</>
)}
</FormGroup>
{birthdayRequired && (
<BirthdayInput

View file

@ -1,28 +1,19 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Redirect } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modals';
import { BigCard } from 'soapbox/components/big-card';
import { Text } from 'soapbox/components/ui';
import { useAppDispatch, useFeatures, useInstance, useRegistrationStatus } from 'soapbox/hooks';
import { useInstance, useRegistrationStatus } from 'soapbox/hooks';
import RegistrationForm from './registration-form';
const RegistrationPage: React.FC = () => {
const instance = useInstance();
const { isOpen } = useRegistrationStatus();
const { nostrSignup } = useFeatures();
const dispatch = useAppDispatch();
if (nostrSignup) {
setTimeout(() => dispatch(openModal('NOSTR_SIGNUP')), 100);
return <Redirect to='/' />;
}
if (!isOpen) {
return (
<BigCard title={<FormattedMessage id='registration.closed_title' defaultMessage='Registrations Closed' />}>
<BigCard title={<FormattedMessage id='registration.closed_title' defaultMessage='Registrations closed' />}>
<Text theme='muted' align='center'>
<FormattedMessage
id='registration.closed_message'

View file

@ -5,7 +5,6 @@ import { openModal } from 'soapbox/actions/modals';
import { useAppDispatch, useAppSelector, useCompose, useFeatures, useOwnAccount } from 'soapbox/hooks';
import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose';
import { makeGetStatus } from 'soapbox/selectors';
import { isPubkey } from 'soapbox/utils/nostr';
import type { Status as StatusEntity } from 'soapbox/types/entities';
@ -56,7 +55,7 @@ const ReplyMentions: React.FC<IReplyMentions> = ({ composeId }) => {
const username = acct.split('@')[0];
return (
<span className='inline-block text-primary-600 no-underline [direction:ltr] hover:text-primary-700 hover:underline dark:text-accent-blue dark:hover:text-accent-blue'>
@{isPubkey(username) ? username.slice(0, 8) : username}
@{username}
</span>
);
}).toArray();

View file

@ -1,122 +0,0 @@
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/outline/at.svg')} />
<span>{instance.domain}</span>
</HStack>
)}
{...props}
/>
);
};
export default EditIdentity;

View file

@ -54,7 +54,6 @@ const messages = defineMessages({
bioPlaceholder: { id: 'edit_profile.fields.bio_placeholder', defaultMessage: 'Tell us about yourself.' },
displayNamePlaceholder: { id: 'edit_profile.fields.display_name_placeholder', defaultMessage: 'Name' },
locationPlaceholder: { id: 'edit_profile.fields.location_placeholder', defaultMessage: 'Location' },
nip05Placeholder: { id: 'edit_profile.fields.nip05_placeholder', defaultMessage: 'user@{domain}' },
cancel: { id: 'common.cancel', defaultMessage: 'Cancel' },
});
@ -75,11 +74,6 @@ interface AccountCredentialsSource {
sensitive?: boolean;
/** Default language to use for authored statuses. (ISO 6391) */
language?: string;
/** Nostr metadata. */
nostr?: {
/** Nostr NIP-05 identifier. */
nip05?: string;
};
}
/**
@ -123,8 +117,6 @@ interface AccountCredentials {
location?: string;
/** User's birthday. */
birthday?: string;
/** Nostr NIP-05 identifier. */
nip05?: string;
}
/** Convert an account into an update_credentials request object. */
@ -146,7 +138,6 @@ const accountToCredentials = (account: Account): AccountCredentials => {
hide_follows_count: hideNetwork,
location: account.location,
birthday: account.pleroma?.birthday ?? undefined,
nip05: account.source?.nostr?.nip05 ?? '',
};
};
@ -313,19 +304,6 @@ const EditProfile: React.FC = () => {
/>
</FormGroup>
{features.nip05 && (
<FormGroup
labelText={<FormattedMessage id='edit_profile.fields.nip05_label' defaultMessage='Username' />}
>
<Input
type='text'
value={data.nip05}
onChange={handleTextChange('nip05')}
placeholder={intl.formatMessage(messages.nip05Placeholder, { domain: instance.domain })}
/>
</FormGroup>
)}
{features.birthdays && (
<FormGroup
labelText={<FormattedMessage id='edit_profile.fields.birthday_label' defaultMessage='Birthday' />}

View file

@ -1,80 +0,0 @@
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

@ -1,74 +0,0 @@
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,34 +0,0 @@
import { nip19 } from 'nostr-tools';
import React from 'react';
import { Redirect } from 'react-router-dom';
import MissingIndicator from 'soapbox/components/missing-indicator';
interface INIP19Redirect {
params: {
bech32: string;
};
}
const Bech32Redirect: React.FC<INIP19Redirect> = ({ params }) => {
try {
const result = nip19.decode(params.bech32);
switch (result.type) {
case 'npub':
case 'nprofile':
return <Redirect to={`/@${params.bech32}`} />;
case 'note':
return <Redirect to={`/posts/${result.data}`} />;
case 'nevent':
return <Redirect to={`/posts/${result.data.id}`} />;
default:
return <MissingIndicator />;
}
} catch (e) {
return <MissingIndicator />;
}
};
export default Bech32Redirect;

View file

@ -1,112 +0,0 @@
import { NSchema as n, NostrSigner, NSecSigner } from '@soapbox/nspec';
import { WebLock } from '@soapbox/weblock';
import { getPublicKey, nip19 } from 'nostr-tools';
import { z } from 'zod';
/**
* Gets Nostr keypairs from storage and returns a `Map`-like object of signers.
* When instantiated, it will lock the storage key to prevent tampering.
* Changes to the object will sync to storage.
*/
export class NKeyStorage implements ReadonlyMap<string, NostrSigner> {
#keypairs = new Map<string, Uint8Array>();
#storage: Storage;
#storageKey: string;
constructor(storage: Storage, storageKey: string) {
this.#storage = storage;
this.#storageKey = storageKey;
const data = this.#storage.getItem(storageKey);
WebLock.storages.lockKey(storageKey);
try {
const nsecs = new Set(this.#dataSchema().parse(data));
for (const nsec of nsecs) {
const { data: secretKey } = nip19.decode(nsec);
const pubkey = getPublicKey(secretKey);
this.#keypairs.set(pubkey, secretKey);
}
} catch (e) {
this.clear();
}
}
#dataSchema(): z.ZodType<`nsec1${string}`[]> {
return n.json().pipe(n.bech32('nsec').array());
}
#syncStorage() {
const secretKeys = [...this.#keypairs.values()].map(nip19.nsecEncode);
this.#storage.setItem(this.#storageKey, JSON.stringify(secretKeys));
}
get size(): number {
return this.#keypairs.size;
}
clear(): void {
this.#keypairs.clear();
this.#syncStorage();
}
delete(pubkey: string): boolean {
const result = this.#keypairs.delete(pubkey);
this.#syncStorage();
return result;
}
forEach(callbackfn: (signer: NostrSigner, pubkey: string, map: typeof this) => void, thisArg?: any): void {
for (const [pubkey] of this.#keypairs) {
const signer = this.get(pubkey);
if (signer) {
callbackfn.call(thisArg, signer, pubkey, this);
}
}
}
get(pubkey: string): NostrSigner | undefined {
const secretKey = this.#keypairs.get(pubkey);
if (secretKey) {
return new NSecSigner(secretKey);
}
}
has(pubkey: string): boolean {
return this.#keypairs.has(pubkey);
}
add(secretKey: Uint8Array): NostrSigner {
const pubkey = getPublicKey(secretKey);
this.#keypairs.set(pubkey, secretKey);
this.#syncStorage();
return this.get(pubkey)!;
}
*entries(): IterableIterator<[string, NostrSigner]> {
for (const [pubkey] of this.#keypairs) {
yield [pubkey, this.get(pubkey)!];
}
}
*keys(): IterableIterator<string> {
for (const pubkey of this.#keypairs.keys()) {
yield pubkey;
}
}
*values(): IterableIterator<NostrSigner> {
for (const pubkey of this.#keypairs.keys()) {
yield this.get(pubkey)!;
}
}
[Symbol.iterator](): IterableIterator<[string, NostrSigner]> {
return this.entries();
}
[Symbol.toStringTag] = 'NKeyStorage';
}

View file

@ -1,63 +0,0 @@
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 nset = useRef<NSet>(new NSet());
const forceUpdate = useForceUpdate();
const [closed, setClosed] = useState(false);
const [eose, setEose] = useState(false);
const controller = useRef<AbortController>(new AbortController());
const signal = controller.current.signal;
const value = useValue(filters);
useEffect(() => {
if (relay && value.length) {
(async () => {
for await (const msg of relay.req(value, { signal })) {
if (msg[0] === 'EVENT') {
nset.current.add(msg[2]);
forceUpdate();
} else if (msg[0] === 'EOSE') {
setEose(true);
} else if (msg[0] === 'CLOSED') {
setClosed(true);
break;
}
}
})();
}
return () => {
controller.current.abort();
controller.current = new AbortController();
setEose(false);
setClosed(false);
};
}, [relay, value]);
return {
events: [...nset.current],
eose,
closed,
};
}
/** Preserves the memory reference of a value across re-renders. */
function useValue<T>(value: T): T {
const ref = useRef<T>(value);
if (!isEqual(ref.current, value)) {
ref.current = value;
}
return ref.current;
}

View file

@ -1,6 +0,0 @@
import { NKeyStorage } from './NKeyStorage';
export const NKeys = new NKeyStorage(
localStorage,
'soapbox:nostr:keys',
);

View file

@ -20,8 +20,6 @@ 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' },
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' },
@ -67,12 +65,6 @@ const Settings = () => {
<ListItem label={intl.formatMessage(messages.editProfile)} to='/settings/profile'>
<span className='max-w-full truncate'>{displayName}</span>
</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>
</CardBody>

View file

@ -32,8 +32,6 @@ import {
MentionsModal,
MissingDescriptionModal,
MuteModal,
NostrLoginModal,
NostrSignupModal,
ReactionsModal,
ReblogsModal,
ReplyMentionsModal,
@ -79,8 +77,6 @@ const MODAL_COMPONENTS: Record<string, React.LazyExoticComponent<any>> = {
'MENTIONS': MentionsModal,
'MISSING_DESCRIPTION': MissingDescriptionModal,
'MUTE': MuteModal,
'NOSTR_LOGIN': NostrLoginModal,
'NOSTR_SIGNUP': NostrSignupModal,
'REACTIONS': ReactionsModal,
'REBLOGS': ReblogsModal,
'REPLY_MENTIONS': ReplyMentionsModal,

View file

@ -1,37 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { closeModal } from 'soapbox/actions/modals';
import { nostrExtensionLogIn } from 'soapbox/actions/nostr';
import Stack from 'soapbox/components/ui/stack/stack';
import Text from 'soapbox/components/ui/text/text';
import { useAppDispatch } from 'soapbox/hooks';
const NostrExtensionIndicator: React.FC = () => {
const dispatch = useAppDispatch();
const onClick = () => {
dispatch(nostrExtensionLogIn());
dispatch(closeModal());
};
return (
<Stack space={2} className='rounded-lg bg-gray-100 p-2 dark:bg-gray-800'>
<Text size='xs'>
{window.nostr ? (
<FormattedMessage
id='nostr_extension.found'
defaultMessage='<link>Sign in</link> with browser extension.'
values={{
link: (node) => <button type='button' className='underline' onClick={onClick}>{node}</button>,
}}
/>
) : (
<FormattedMessage id='nostr_extension.not_found' defaultMessage='Browser extension not found.' />
)}
</Text>
</Stack>
);
};
export default NostrExtensionIndicator;

View file

@ -1,30 +0,0 @@
import React, { useState } from 'react';
import ExtensionStep from './steps/extension-step';
import KeyAddStep from './steps/key-add-step';
type Step = 'extension' | 'key-add';
interface INostrLoginModal {
onClose: (type?: string) => void;
step?: Step;
}
const NostrLoginModal: React.FC<INostrLoginModal> = ({ onClose, step: firstStep }) => {
const [step, setStep] = useState<Step>(firstStep ?? (window.nostr ? 'extension' : 'key-add'));
const handleClose = () => onClose('NOSTR_LOGIN');
switch (step) {
case 'extension':
return <ExtensionStep onClickAlt={() => setStep('key-add')} onClose={handleClose} />;
case 'key-add':
return <KeyAddStep onClose={handleClose} />;
default:
return null;
}
};
export default NostrLoginModal;
export type { Step };

View file

@ -1,41 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { nostrExtensionLogIn } from 'soapbox/actions/nostr';
import EmojiGraphic from 'soapbox/components/emoji-graphic';
import { Button, Stack, Modal } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
interface IExtensionStep {
onClickAlt: () => void;
onClose(): void;
}
const ExtensionStep: React.FC<IExtensionStep> = ({ onClickAlt, onClose }) => {
const dispatch = useAppDispatch();
const onClick = () => {
dispatch(nostrExtensionLogIn());
onClose();
};
return (
<Modal title={<FormattedMessage id='nostr_signup.siwe.title' defaultMessage='Sign in' />} onClose={onClose}>
<Stack space={6}>
<EmojiGraphic emoji='🔐' />
<Stack space={3}>
<Button theme='accent' size='lg' onClick={onClick}>
<FormattedMessage id='nostr_signup.siwe.action' defaultMessage='Sign in with extension' />
</Button>
<Button theme='transparent' onClick={onClickAlt}>
<FormattedMessage id='nostr_signup.siwe.alt' defaultMessage='Sign in with key' />
</Button>
</Stack>
</Stack>
</Modal>
);
};
export default ExtensionStep;

View file

@ -1,71 +0,0 @@
import { nip19 } from 'nostr-tools';
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { logInNostr } from 'soapbox/actions/nostr';
import EmojiGraphic from 'soapbox/components/emoji-graphic';
import { Button, Stack, Modal, Input, FormGroup, Form } from 'soapbox/components/ui';
import { NKeys } from 'soapbox/features/nostr/keys';
import { useAppDispatch } from 'soapbox/hooks';
import NostrExtensionIndicator from '../components/nostr-extension-indicator';
interface IKeyAddStep {
onClose(): void;
}
const KeyAddStep: React.FC<IKeyAddStep> = ({ onClose }) => {
const [nsec, setNsec] = useState('');
const [error, setError] = useState<string | undefined>();
const dispatch = useAppDispatch();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNsec(e.target.value);
setError(undefined);
};
const handleSubmit = async () => {
try {
const result = nip19.decode(nsec);
if (result.type === 'nsec') {
const seckey = result.data;
const signer = NKeys.add(seckey);
const pubkey = await signer.getPublicKey();
dispatch(logInNostr(pubkey));
onClose();
}
} catch (e) {
setError('Invalid nsec');
}
};
return (
<Modal title={<FormattedMessage id='nostr_signup.key-add.title' defaultMessage='Import Key' />} onClose={onClose}>
<Stack className='my-3' space={6}>
<NostrExtensionIndicator />
<EmojiGraphic emoji='🔑' />
<Form onSubmit={handleSubmit}>
<Stack space={6}>
<FormGroup labelText='Secret key' errors={error ? [error] : []}>
<Input
value={nsec}
type='password'
onChange={handleChange}
placeholder='nsec1…'
/>
</FormGroup>
<Button theme='accent' size='lg' type='submit' disabled={!nsec}>
Add Key
</Button>
</Stack>
</Form>
</Stack>
</Modal>
);
};
export default KeyAddStep;

View file

@ -1,33 +0,0 @@
import React, { useState } from 'react';
import ExtensionStep from '../nostr-login-modal/steps/extension-step';
import KeyStep from './steps/key-step';
import KeygenStep from './steps/keygen-step';
type Step = 'extension' | 'key' | 'keygen';
interface INostrSignupModal {
onClose: (type?: string) => void;
}
const NostrSigninModal: React.FC<INostrSignupModal> = ({ onClose }) => {
const [step, setStep] = useState<Step>(window.nostr ? 'extension' : 'key');
const handleClose = () => onClose('NOSTR_SIGNUP');
switch (step) {
case 'extension':
return <ExtensionStep onClickAlt={() => setStep('key')} onClose={handleClose} />;
case 'key':
return <KeyStep setStep={setStep} onClose={handleClose} />;
case 'keygen':
return <KeygenStep onClose={handleClose} />;
default:
return null;
}
};
export default NostrSigninModal;
export type { Step };

View file

@ -1,46 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { openModal } from 'soapbox/actions/modals';
import EmojiGraphic from 'soapbox/components/emoji-graphic';
import { Button, Stack, Modal } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
import NostrExtensionIndicator from '../../nostr-login-modal/components/nostr-extension-indicator';
import { Step } from '../nostr-signup-modal';
interface IKeyStep {
setStep(step: Step): void;
onClose(): void;
}
const KeyStep: React.FC<IKeyStep> = ({ setStep, onClose }) => {
const dispatch = useAppDispatch();
const onAltClick = () => {
onClose();
dispatch(openModal('NOSTR_LOGIN', { step: 'key-add' }));
};
return (
<Modal title={<FormattedMessage id='nostr_signup.key.title' defaultMessage='You need a key to continue' />} onClose={onClose}>
<Stack className='my-3' space={6}>
<NostrExtensionIndicator />
<EmojiGraphic emoji='🔑' />
<Stack space={3} alignItems='center'>
<Button theme='accent' size='lg' onClick={() => setStep('keygen')}>
Generate key
</Button>
<Button theme='transparent' onClick={onAltClick}>
I already have a key
</Button>
</Stack>
</Stack>
</Modal>
);
};
export default KeyStep;

View file

@ -1,81 +0,0 @@
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
import React, { useEffect, useMemo, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { fetchAccount } from 'soapbox/actions/accounts';
import { logInNostr } from 'soapbox/actions/nostr';
import CopyableInput from 'soapbox/components/copyable-input';
import EmojiGraphic from 'soapbox/components/emoji-graphic';
import { Button, Stack, Modal, FormGroup, Text, Tooltip } from 'soapbox/components/ui';
import { NKeys } from 'soapbox/features/nostr/keys';
import { useAppDispatch, useInstance } from 'soapbox/hooks';
import { download } from 'soapbox/utils/download';
import { slugify } from 'soapbox/utils/input';
interface IKeygenStep {
onClose(): void;
}
const KeygenStep: React.FC<IKeygenStep> = ({ onClose }) => {
const instance = useInstance();
const dispatch = useAppDispatch();
const secretKey = useMemo(() => generateSecretKey(), []);
const pubkey = useMemo(() => getPublicKey(secretKey), [secretKey]);
const nsec = useMemo(() => nip19.nsecEncode(secretKey), [secretKey]);
const npub = useMemo(() => nip19.npubEncode(pubkey), [pubkey]);
const [downloaded, setDownloaded] = useState(false);
useEffect(() => {
// Pre-fetch into cache.
dispatch(fetchAccount(pubkey)).catch(() => {});
}, [pubkey]);
const handleDownload = () => {
download(nsec, `${slugify(instance.title)}-${npub.slice(5, 9)}.nsec.txt`);
setDownloaded(true);
};
const handleCopy = () => setDownloaded(true);
const handleNext = async () => {
const signer = NKeys.add(secretKey);
const pubkey = await signer.getPublicKey();
dispatch(logInNostr(pubkey));
onClose();
};
return (
<Modal title={<FormattedMessage id='nostr_signup.keygen.title' defaultMessage='Your new key' />} onClose={onClose}>
<Stack className='my-3' space={9}>
<EmojiGraphic emoji='🔑' />
<Stack alignItems='center'>
<Button theme='primary' size='lg' icon={require('@tabler/icons/outline/download.svg')} onClick={handleDownload}>
Download key
</Button>
</Stack>
<FormGroup labelText='Secret key'>
<CopyableInput value={nsec} type='password' onCopy={handleCopy} />
</FormGroup>
<Stack className='rounded-xl bg-gray-100 p-4 dark:bg-gray-800'>
<Text>Back up your secret key in a secure place. If lost, your account cannot be recovered. Never share your secret key with anyone.</Text>
</Stack>
<Stack alignItems='end'>
<Tooltip text='Download your key to continue' disabled={downloaded}>
<Button theme='accent' disabled={!downloaded} size='lg' onClick={handleNext}>
Next
</Button>
</Tooltip>
</Stack>
</Stack>
</Modal>
);
};
export default KeygenStep;

View file

@ -5,7 +5,6 @@ import { Link, Redirect } from 'react-router-dom';
import { logIn, verifyCredentials } from 'soapbox/actions/auth';
import { fetchInstance } from 'soapbox/actions/instance';
import { openModal } from 'soapbox/actions/modals';
import { openSidebar } from 'soapbox/actions/sidebar';
import SiteLogo from 'soapbox/components/site-logo';
import { Avatar, Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
@ -39,10 +38,6 @@ const Navbar = () => {
const onOpenSidebar = () => dispatch(openSidebar());
const handleNostrLogin = async () => {
dispatch(openModal('NOSTR_LOGIN'));
};
const handleSubmit: React.FormEventHandler = (event) => {
event.preventDefault();
setLoading(true);
@ -112,61 +107,49 @@ const Navbar = () => {
</div>
) : (
<>
{features.nostrSignup ? (
<div className='hidden items-center xl:flex'>
<Button
theme='primary'
onClick={handleNostrLogin}
disabled={isLoading}
>
{intl.formatMessage(messages.login)}
</Button>
</div>
) : (
<Form className='hidden items-center space-x-2 xl:flex rtl:space-x-reverse' onSubmit={handleSubmit}>
<Input
required
value={username}
onChange={(event) => setUsername(event.target.value)}
type='text'
placeholder={intl.formatMessage(features.logInWithUsername ? messages.username : messages.email)}
className='max-w-[200px]'
/>
<Form className='hidden items-center space-x-2 xl:flex rtl:space-x-reverse' onSubmit={handleSubmit}>
<Input
required
value={username}
onChange={(event) => setUsername(event.target.value)}
type='text'
placeholder={intl.formatMessage(features.logInWithUsername ? messages.username : messages.email)}
className='max-w-[200px]'
/>
<Input
required
value={password}
onChange={(event) => setPassword(event.target.value)}
type='password'
placeholder={intl.formatMessage(messages.password)}
className='max-w-[200px]'
/>
<Input
required
value={password}
onChange={(event) => setPassword(event.target.value)}
type='password'
placeholder={intl.formatMessage(messages.password)}
className='max-w-[200px]'
/>
<Link to='/reset-password'>
<Tooltip text={intl.formatMessage(messages.forgotPassword)}>
<IconButton
src={require('@tabler/icons/outline/help.svg')}
className='cursor-pointer bg-transparent text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-200'
iconClassName='h-5 w-5'
/>
</Tooltip>
</Link>
<Link to='/reset-password'>
<Tooltip text={intl.formatMessage(messages.forgotPassword)}>
<IconButton
src={require('@tabler/icons/outline/help.svg')}
className='cursor-pointer bg-transparent text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-200'
iconClassName='h-5 w-5'
/>
</Tooltip>
</Link>
<Button
theme='primary'
type='submit'
disabled={isLoading}
>
{intl.formatMessage(messages.login)}
</Button>
</Form>
)}
<Button
theme='primary'
type='submit'
disabled={isLoading}
>
{intl.formatMessage(messages.login)}
</Button>
</Form>
<div className='space-x-1.5 xl:hidden'>
<Button
theme='tertiary'
size='sm'
{...(features.nostrSignup ? { onClick: handleNostrLogin } : { to: '/login' })}
to='/login'
>
<FormattedMessage id='account.login' defaultMessage='Log in' />
</Button>

View file

@ -1,16 +1,13 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { openModal } from 'soapbox/actions/modals';
import { Button, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useFeatures, useInstance, useRegistrationStatus } from 'soapbox/hooks';
import { useAppSelector, useInstance, useRegistrationStatus } from 'soapbox/hooks';
const SignUpPanel = () => {
const instance = useInstance();
const { nostrSignup } = useFeatures();
const { isOpen } = useRegistrationStatus();
const me = useAppSelector((state) => state.me);
const dispatch = useAppDispatch();
if (me || !isOpen) return null;
@ -28,8 +25,7 @@ const SignUpPanel = () => {
<Button
theme='primary'
onClick={nostrSignup ? () => dispatch(openModal('NOSTR_SIGNUP')) : undefined}
to={nostrSignup ? undefined : '/signup'}
to='/signup'
block
>
<FormattedMessage id='account.register' defaultMessage='Sign up' />

View file

@ -14,7 +14,6 @@ import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses';
import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions';
import { expandHomeTimeline } from 'soapbox/actions/timelines';
import { useUserStream } from 'soapbox/api/hooks';
import { useSignerStream } from 'soapbox/api/hooks/nostr/useSignerStream';
import SidebarNavigation from 'soapbox/components/sidebar-navigation';
import ThumbNavigation from 'soapbox/components/thumb-navigation';
import { Layout } from 'soapbox/components/ui';
@ -126,10 +125,7 @@ import {
ExternalLogin,
LandingTimeline,
BookmarkFolders,
EditIdentity,
Domains,
NostrRelays,
Bech32Redirect,
Relays,
Rules,
DraftStatuses,
@ -294,13 +290,11 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
<WrappedRoute path='/draft_statuses' page={DefaultPage} component={DraftStatuses} 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.importData && <WrappedRoute path='/settings/import' page={DefaultPage} component={ImportData} 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.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/password' page={DefaultPage} component={EditPassword} content={children} />
<WrappedRoute path='/settings/account' page={DefaultPage} component={DeleteAccount} content={children} />
@ -348,8 +342,6 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
<Redirect from='/auth/password/new' to='/reset-password' />
<Redirect from='/auth/password/edit' to={`/edit-password${search}`} />
<WrappedRoute path='/:bech32([\x21-\x7E]{1,83}1[023456789acdefghjklmnpqrstuvwxyz]{6,})' publicRoute page={EmptyPage} component={Bech32Redirect} content={children} />
<WrappedRoute page={EmptyPage} component={GenericNotFound} content={children} />
</Switch>
);
@ -443,7 +435,6 @@ const UI: React.FC<IUI> = ({ children }) => {
}, []);
useUserStream();
useSignerStream();
// The user has logged in
useEffect(() => {

View file

@ -151,16 +151,11 @@ export const EditAnnouncementModal = lazy(() => import('soapbox/features/ui/comp
export const FollowedTags = lazy(() => import('soapbox/features/followed-tags'));
export const AccountNotePanel = lazy(() => import('soapbox/features/ui/components/panels/account-note-panel'));
export const ComposeEditor = lazy(() => import('soapbox/features/compose/editor'));
export const NostrSignupModal = lazy(() => import('soapbox/features/ui/components/modals/nostr-signup-modal/nostr-signup-modal'));
export const NostrLoginModal = lazy(() => import('soapbox/features/ui/components/modals/nostr-login-modal/nostr-login-modal'));
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'));
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'));
export const Bech32Redirect = lazy(() => import('soapbox/features/nostr/Bech32Redirect'));
export const Relays = lazy(() => import('soapbox/features/admin/relays'));
export const Rules = lazy(() => import('soapbox/features/admin/rules'));
export const EditRuleModal = lazy(() => import('soapbox/features/ui/components/modals/edit-rule-modal'));

View file

@ -2,7 +2,6 @@ import { QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import { Provider } from 'react-redux';
import { NostrProvider } from 'soapbox/contexts/nostr-context';
import { StatProvider } from 'soapbox/contexts/stat-context';
import { createGlobals } from 'soapbox/globals';
import { queryClient } from 'soapbox/queries/client';
@ -30,13 +29,11 @@ const Soapbox: React.FC = () => {
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<StatProvider>
<NostrProvider>
<SoapboxHead>
<SoapboxLoad>
<SoapboxMount />
</SoapboxLoad>
</SoapboxHead>
</NostrProvider>
<SoapboxHead>
<SoapboxLoad>
<SoapboxMount />
</SoapboxLoad>
</SoapboxHead>
</StatProvider>
</QueryClientProvider>
</Provider>

View file

@ -286,7 +286,6 @@
"chat_settings.leave.message": "Are you sure you want to leave this chat? Messages will be deleted for you and this chat will be removed from your inbox.",
"chat_settings.options.block_user": "Block @{acct}",
"chat_settings.options.leave_chat": "Leave Chat",
"chat_settings.options.report_user": "Report @{acct}",
"chat_settings.options.unblock_user": "Unblock @{acct}",
"chat_settings.title": "Chat Details",
"chat_settings.unblock.confirm": "Unblock",
@ -296,7 +295,6 @@
"chats.actions.delete": "Delete for both",
"chats.actions.delete_for_me": "Delete for me",
"chats.actions.more": "More",
"chats.actions.report": "Report",
"chats.dividers.today": "Today",
"chats.main.blankslate.new_chat": "Message someone",
"chats.main.blankslate.subtitle": "Search for someone to chat with",
@ -473,7 +471,6 @@
"compose_form.spoiler_placeholder": "Write your warning here (optional)",
"compose_form.spoiler_remove": "Remove sensitive",
"compose_form.spoiler_title": "Sensitive content",
"compose_group.share_to_followers": "Share with my followers",
"confirmation_modal.cancel": "Cancel",
"confirmations.admin.deactivate_user.confirm": "Deactivate @{name}",
"confirmations.admin.deactivate_user.heading": "Deactivate @{acct}",
@ -639,9 +636,6 @@
"edit_profile.fields.meta_fields.content_placeholder": "Content",
"edit_profile.fields.meta_fields.label_placeholder": "Label",
"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.header": "Edit Profile",
"edit_profile.hints.accepts_email_list": "Opt-in to news and marketing updates.",
@ -840,14 +834,12 @@
"group.promote.admin.confirmation.message": "Are you sure you want to assign the admin role to @{name}?",
"group.promote.admin.confirmation.title": "Assign Admin Role",
"group.promote.admin.success": "@{name} is now an admin",
"group.report.label": "Report",
"group.role.admin": "Admin",
"group.role.owner": "Owner",
"group.share.label": "Share",
"group.tabs.all": "All",
"group.tabs.media": "Media",
"group.tabs.members": "Members",
"group.tabs.tags": "Topics",
"group.update.success": "Group successfully saved",
"group.upload_avatar": "Upload avatar",
"group.upload_banner": "Upload photo",
@ -1072,18 +1064,6 @@
"new_group_panel.action": "Create Group",
"new_group_panel.subtitle": "Can't find what you're looking for? Start your own private or public group.",
"new_group_panel.title": "Create Group",
"nostr_extension.found": "<link>Sign in</link> 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",
"nostr_signup.siwe.action": "Sign in with extension",
"nostr_signup.siwe.alt": "Sign in with key",
"nostr_signup.siwe.title": "Sign in",
"notification.favourite": "{name} liked your post",
"notification.follow": "{name} followed you",
"notification.follow_request": "{name} has requested to follow you",
@ -1216,7 +1196,7 @@
"registration.captcha.hint": "Click the image to get a new captcha",
"registration.captcha.placeholder": "Enter the pictured text",
"registration.closed_message": "{instance} is not accepting new members",
"registration.closed_title": "Registrations Closed",
"registration.closed_title": "Registrations closed",
"registration.confirmation_modal.close": "Close",
"registration.fields.confirm_placeholder": "Password (again)",
"registration.fields.email_placeholder": "E-Mail address",
@ -1329,7 +1309,6 @@
"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",
@ -1431,8 +1410,6 @@
"status.mute_conversation": "Mute Conversation",
"status.open": "Show Post Details",
"status.pin": "Pin on profile",
"status.pin_to_group": "Pin to Group",
"status.pin_to_group.success": "Pinned to Group!",
"status.pinned": "Pinned post",
"status.quote": "Quote post",
"status.quote_tombstone": "Post is unavailable.",
@ -1470,8 +1447,6 @@
"status.unbookmarked": "Bookmark removed.",
"status.unmute_conversation": "Unmute Conversation",
"status.unpin": "Unpin from profile",
"status.unpin_to_group": "Unpin from Group",
"status.zap": "Zap",
"status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}",
"statuses.quote_tombstone": "Post is unavailable.",
"statuses.tombstone": "One or more posts are unavailable.",

View file

@ -17,7 +17,6 @@ import '@fontsource/inter/900.css';
import '@fontsource/roboto-mono/400.css';
import 'line-awesome/dist/font-awesome-line-awesome/css/all.css';
import 'react-datepicker/dist/react-datepicker.css';
import 'soapbox/features/nostr/keys';
import './iframe';
import './styles/i18n/arabic.css';

View file

@ -82,7 +82,6 @@ export const StatusRecord = ImmutableRecord({
uri: '',
url: '',
visibility: 'public' as StatusVisibility,
zapped: false,
event: null as ReturnType<typeof EventRecord> | null,
// Internal fields

View file

@ -30,8 +30,6 @@ import {
DISLIKE_REQUEST,
UNDISLIKE_REQUEST,
DISLIKE_FAIL,
ZAP_REQUEST,
ZAP_FAIL,
} from '../actions/interactions';
import {
STATUS_CREATE_REQUEST,
@ -234,18 +232,6 @@ const simulateDislike = (
return state.set(statusId, updatedStatus);
};
/** Simulate zap of status for optimistic interactions */
const simulateZap = (state: State, statusId: string, zapped: boolean): State => {
const status = state.get(statusId);
if (!status) return state;
const updatedStatus = status.merge({
zapped,
});
return state.set(statusId, updatedStatus);
};
interface Translation {
content: string;
detected_source_language: string;
@ -300,10 +286,6 @@ export default function statuses(state = initialState, action: AnyAction): State
return state.get(action.status.id) === undefined ? state : state.setIn([action.status.id, 'favourited'], false);
case DISLIKE_FAIL:
return state.get(action.status.id) === undefined ? state : state.setIn([action.status.id, 'disliked'], false);
case ZAP_REQUEST:
return simulateZap(state, action.status.id, true);
case ZAP_FAIL:
return simulateZap(state, action.status.id, false);
case REBLOG_REQUEST:
return state.setIn([action.status.id, 'reblogged'], true);
case REBLOG_FAIL:

View file

@ -1,4 +1,3 @@
import { NSchema as n } from '@soapbox/nspec';
import escapeTextContentForBrowser from 'escape-html';
import DOMPurify from 'isomorphic-dompurify';
import z from 'zod';
@ -8,7 +7,7 @@ import { unescapeHTML } from 'soapbox/utils/html';
import { customEmojiSchema } from './custom-emoji';
import { relationshipSchema } from './relationship';
import { coerceObject, contentSchema, filteredArray, makeCustomEmojiMap } from './utils';
import { contentSchema, filteredArray, makeCustomEmojiMap } from './utils';
import type { Resolve } from 'soapbox/utils/types';
@ -40,10 +39,6 @@ const baseAccountSchema = z.object({
created_at: z.string().datetime().catch(new Date().toUTCString()),
discoverable: z.boolean().catch(false),
display_name: z.string().catch(''),
ditto: coerceObject({
accepts_zaps: z.boolean().catch(false),
is_registered: z.boolean().catch(false),
}),
emojis: filteredArray(customEmojiSchema),
fields: filteredArray(fieldSchema),
followers_count: z.number().catch(0),
@ -61,9 +56,6 @@ const baseAccountSchema = z.object({
z.string(),
z.null(),
]).catch(null),
nostr: coerceObject({
pubkey: n.id().optional().catch(undefined),
}),
note: contentSchema,
/** Fedibird extra settings. */
other_settings: z.object({
@ -102,10 +94,6 @@ const baseAccountSchema = z.object({
pleroma: z.object({
discoverable: z.boolean().catch(true),
}).optional().catch(undefined),
sms_verified: z.boolean().catch(false),
nostr: z.object({
nip05: z.string().optional().catch(undefined),
}).optional().catch(undefined),
}).optional().catch(undefined),
statuses_count: z.number().catch(0),
suspended: z.boolean().catch(false),

View file

@ -79,10 +79,6 @@ const contactSchema = coerceObject({
email: z.string().email().catch(''),
});
const nostrSchema = coerceObject({
pubkey: z.string(),
relay: z.string().url(),
});
const pleromaSchema = coerceObject({
metadata: coerceObject({
@ -183,7 +179,6 @@ const instanceV1Schema = coerceObject({
languages: z.string().array().catch([]),
max_media_attachments: z.number().optional().catch(undefined),
max_toot_chars: z.number().optional().catch(undefined),
nostr: nostrSchema.optional().catch(undefined),
pleroma: pleromaSchema,
poll_limits: pleromaPollLimitsSchema,
registrations: z.boolean().catch(false),
@ -271,7 +266,6 @@ const instanceSchema = z.preprocess((data: any) => {
feature_quote: z.boolean().catch(false),
fedibird_capabilities: z.array(z.string()).catch([]),
languages: z.string().array().catch([]),
nostr: nostrSchema.optional().catch(undefined),
pleroma: pleromaSchema,
registrations: registrations,
rules: filteredArray(ruleSchema),

View file

@ -1,42 +0,0 @@
import { verifyEvent } from 'nostr-tools';
import { z } from 'zod';
/** Schema to validate Nostr hex IDs such as event IDs and pubkeys. */
const nostrIdSchema = z.string().regex(/^[0-9a-f]{64}$/);
/** Nostr kinds are positive integers. */
const kindSchema = z.number().int().nonnegative();
/** Nostr event template schema. */
const eventTemplateSchema = z.object({
kind: kindSchema,
tags: z.array(z.array(z.string())),
content: z.string(),
created_at: z.number(),
});
/** Nostr event schema. */
const eventSchema = eventTemplateSchema.extend({
id: nostrIdSchema,
pubkey: nostrIdSchema,
sig: z.string(),
});
/** Nostr event schema that also verifies the event's signature. */
const signedEventSchema = eventSchema.refine(verifyEvent);
/** NIP-46 request content schema. */
const connectRequestSchema = z.object({
id: z.string(),
method: z.string(),
params: z.string().array(),
});
/** NIP-47 signer response. */
const nwcRequestSchema = z.object({
method: z.literal('pay_invoice'),
params: z.object({
invoice: z.string(),
}),
});
export { nostrIdSchema, kindSchema, eventSchema, signedEventSchema, connectRequestSchema, nwcRequestSchema };

View file

@ -68,7 +68,6 @@ const baseStatusSchema = z.object({
uri: z.string().url().catch(''),
url: z.string().url().catch(''),
visibility: z.string().catch('public'),
zapped: z.coerce.boolean(),
});
type BaseStatus = z.infer<typeof baseStatusSchema>;

View file

@ -1,7 +0,0 @@
import type { NostrSigner } from '@soapbox/nspec';
declare global {
interface Window {
nostr?: NostrSigner;
}
}

View file

@ -14,12 +14,6 @@ const overrides = custom('features');
/** Truthy array convenience function */
const any = (arr: Array<any>): boolean => arr.some(Boolean);
/**
* Ditto, a Nostr server with Mastodon API.
* @see {@link https://gitlab.com/soapbox-pub/ditto}
*/
export const DITTO = 'Ditto';
/**
* Firefish, a fork of Misskey. Formerly known as Calckey.
* @see {@link https://joinfirefish.org/}
@ -144,7 +138,6 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
v.software === PLEROMA && gte(v.version, '2.4.50'),
v.software === TAKAHE && gte(v.version, '0.6.1'),
v.software === DITTO,
]),
/**
@ -256,7 +249,6 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === PLEROMA && gte(v.version, '0.9.9'),
v.software === PIXELFED,
v.software === TAKAHE && gte(v.version, '0.9.0'),
v.software === DITTO,
]),
/**
@ -513,7 +505,6 @@ const getInstanceFeatures = (instance: Instance) => {
*/
frontendConfigurations: any([
v.software === PLEROMA,
v.software === DITTO,
]),
/**
@ -604,7 +595,6 @@ const getInstanceFeatures = (instance: Instance) => {
* @see POST /api/v1/admin/accounts/:account_id/approve
*/
mastodonAdmin: any([
v.software === DITTO,
v.software === MASTODON && gte(v.compatVersion, '2.9.1'),
v.software === PLEROMA && v.build === REBASED && gte(v.version, '2.4.50'),
]),
@ -644,27 +634,6 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === TAKAHE,
]),
/**
* Can set a Nostr username.
* @see PATCH /api/v1/accounts/update_credentials
*/
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
*/
nostrSign: v.software === DITTO,
/**
* Whether the backend uses Ditto's Nosteric way of registration.
* @see POST /api/v1/accounts
*/
nostrSignup: v.software === DITTO,
/**
* Add private notes to accounts.
* @see POST /api/v1/accounts/:id/note
@ -708,7 +677,7 @@ const getInstanceFeatures = (instance: Instance) => {
* Can set privacy scopes on statuses.
* @see POST /api/v1/statuses
*/
privacyScopes: v.software !== DITTO,
privacyScopes: true,
/**
* A directory of discoverable profiles from the instance.
@ -743,7 +712,6 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === PLEROMA,
v.software === TAKAHE,
v.software === WILDEBEEST,
v.software === DITTO,
]),
/**
@ -888,7 +856,6 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
v.software === ICESHRIMP,
v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
v.software === DITTO,
]),
/**

View file

@ -1,6 +0,0 @@
/** Check whether the given input is a valid Nostr hexadecimal pubkey. */
const isPubkey = (value: string) => /^[0-9a-f]{64}$/i.test(value);
export {
isPubkey,
};

View file

@ -1,9 +0,0 @@
import * as Comlink from 'comlink';
import type { PowWorker } from './workers/pow.worker';
const powWorker = Comlink.wrap<typeof PowWorker>(
new Worker(new URL('./workers/pow.worker.ts', import.meta.url), { type: 'module' }),
);
export { powWorker };

View file

@ -1,10 +0,0 @@
import * as Comlink from 'comlink';
import { nip13, type UnsignedEvent } from 'nostr-tools';
export const PowWorker = {
mine(event: UnsignedEvent, difficulty: number) {
return nip13.minePow(event, difficulty);
},
};
Comlink.expose(PowWorker);

174
yarn.lock
View file

@ -1853,48 +1853,7 @@
lodash "^4.17.21"
mousetrap "^1.6.5"
"@noble/ciphers@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.2.0.tgz#a12cda60f3cf1ab5d7c77068c3711d2366649ed7"
integrity sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==
"@noble/ciphers@^0.5.1":
version "0.5.1"
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.5.1.tgz#292f388b69c9ed80d49dca1a5cbfd4ff06852111"
integrity sha512-aNE06lbe36ifvMbbWvmmF/8jx6EQPu2HVg70V95T+iGjOuYwPpAccwAQc2HlXO2D0aiQ3zavbMga4jjWnrpiPA==
"@noble/curves@1.1.0", "@noble/curves@~1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==
dependencies:
"@noble/hashes" "1.3.1"
"@noble/curves@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35"
integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==
dependencies:
"@noble/hashes" "1.3.2"
"@noble/curves@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e"
integrity sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==
dependencies:
"@noble/hashes" "1.3.3"
"@noble/hashes@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
"@noble/hashes@1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
"@noble/hashes@1.3.3", "@noble/hashes@^1.3.3", "@noble/hashes@~1.3.2":
"@noble/hashes@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699"
integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==
@ -2148,50 +2107,6 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.1.tgz#d146db7a5949e10837b323ce933ed882ac878262"
integrity sha512-PyJsSsafjmIhVgaI1Zdj7m8BB8mMckFah/xbpplObyHfiXzKcI5UOUXRyOdHW7nz4DpMCuzLnF7v5IWHenCwYA==
"@scure/base@1.1.1", "@scure/base@~1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
"@scure/base@^1.1.5", "@scure/base@~1.1.4":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157"
integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==
"@scure/bip32@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10"
integrity sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==
dependencies:
"@noble/curves" "~1.1.0"
"@noble/hashes" "~1.3.1"
"@scure/base" "~1.1.0"
"@scure/bip32@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.3.tgz#a9624991dc8767087c57999a5d79488f48eae6c8"
integrity sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==
dependencies:
"@noble/curves" "~1.3.0"
"@noble/hashes" "~1.3.2"
"@scure/base" "~1.1.4"
"@scure/bip39@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a"
integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==
dependencies:
"@noble/hashes" "~1.3.0"
"@scure/base" "~1.1.0"
"@scure/bip39@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.2.tgz#f3426813f4ced11a47489cbcf7294aa963966527"
integrity sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==
dependencies:
"@noble/hashes" "~1.3.2"
"@scure/base" "~1.1.4"
"@sentry-internal/tracing@7.74.1":
version "7.74.1"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.74.1.tgz#55ff387e61d2c9533a9a0d099d376332426c8e08"
@ -2276,25 +2191,6 @@
raf "^3.4.0"
responsive-gamepad "1.1.0"
"@soapbox/nspec@npm:@jsr/soapbox__nspec":
version "0.6.0"
resolved "https://npm.jsr.io/~/6/@jsr/soapbox__nspec/0.6.0.tgz#60A75BCDBEC1B76DFA91BEFDF5505CCB8ADDAD3B"
integrity sha512-HY+MssBjm532J9SAqLek8YGxBlEaXdT1Eek3bOWkq4uLJxipJhYkdQrW+NzXhfVvGZUt6YXBobeSqRQx1JFgkQ==
dependencies:
"@noble/hashes" "^1.3.3"
"@scure/base" "^1.1.5"
"@scure/bip32" "^1.3.3"
"@scure/bip39" "^1.2.2"
lru-cache "^10.2.0"
nostr-tools "^2.3.1"
websocket-ts "^2.1.5"
zod "^3.22.4"
"@soapbox/weblock@npm:@jsr/soapbox__weblock":
version "0.1.0"
resolved "https://npm.jsr.io/~/7/@jsr/soapbox__weblock/0.1.0.tgz#749AEE0872D23CC4E37366D5F0D092B87986C5E1"
integrity sha512-FLLJL6xYk+k7f2bMXJ1nbcn3lhbEZXA0yboKLm8wns0hrcoEDOrWwmxkYF7xpVRndiAzFBctBGVbIAa3sA72ew==
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@ -6395,11 +6291,6 @@ lowercase-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
lru-cache@^10.2.0:
version "10.2.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
lru-cache@^4.1.2:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
@ -6690,59 +6581,6 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
nostr-machina@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/nostr-machina/-/nostr-machina-0.1.0.tgz#e111e86eb51655e5de31862174d23de184e6e98a"
integrity sha512-sNswM9vgq7R/96YIJKZOlG0M/m2mZrb1TiPA7hpOMrnWHBGdDuAeON0vLWJaGbvpuDKYQ1b5ZiLZ8HM3EZPevw==
dependencies:
nostr-tools "^1.14.0"
zod "^3.21.0"
nostr-tools@^1.14.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.16.0.tgz#5867f1d8bd055a5a3b27aadb199457dceb244314"
integrity sha512-sx/aOl0gmkeHVoIVbyOhEQhzF88NsrBXMC8bsjhPASqA6oZ8uSOAyEGgRLMfC3SKgzQD5Gr6KvDoAahaD6xKcg==
dependencies:
"@noble/ciphers" "^0.2.0"
"@noble/curves" "1.1.0"
"@noble/hashes" "1.3.1"
"@scure/base" "1.1.1"
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
nostr-tools@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.3.0.tgz#687d1af486a21e3e4805f0d4167c01221d871e65"
integrity sha512-jWD71y9JJ7DJ5/Si/DhREkjwyCWgMmY7x8qXfA9xC1HeosoGnaXuyYtspfYuiy8B8B2969C1iR6rWt6Fyf3IaA==
dependencies:
"@noble/ciphers" "^0.5.1"
"@noble/curves" "1.2.0"
"@noble/hashes" "1.3.1"
"@scure/base" "1.1.1"
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
optionalDependencies:
nostr-wasm v0.1.0
nostr-tools@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.3.1.tgz#348d3c4aab0ab00716f93d6e2a72333d8c7da982"
integrity sha512-qjKx2C3EzwiQOe2LPSPyCnp07pGz1pWaWjDXcm+L2y2c8iTECbvlzujDANm3nJUjWL5+LVRUVDovTZ1a/DC4Bg==
dependencies:
"@noble/ciphers" "^0.5.1"
"@noble/curves" "1.2.0"
"@noble/hashes" "1.3.1"
"@scure/base" "1.1.1"
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
optionalDependencies:
nostr-wasm v0.1.0
nostr-wasm@v0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"
integrity sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@ -9422,11 +9260,6 @@ webpack-sources@^3.2.3:
watchpack "^2.4.0"
webpack-sources "^3.2.3"
websocket-ts@^2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/websocket-ts/-/websocket-ts-2.1.5.tgz#b6b51f0afca89d6bc7ff71c9e74540f19ae0262c"
integrity sha512-rCNl9w6Hsir1azFm/pbjBEFzLD/gi7Th5ZgOxMifB6STUfTSovYAzryWw0TRvSZ1+Qu1Z5Plw4z42UfTNA9idA==
whatwg-encoding@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5"
@ -9800,11 +9633,6 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
zod@^3.21.0:
version "3.22.3"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.3.tgz#2fbc96118b174290d94e8896371c95629e87a060"
integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==
zod@^3.22.4:
version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"