Merge pull request #1 from mkljczk/denostrify
Remove Nostr-related stuff
This commit is contained in:
commit
aedf097ec0
49 changed files with 88 additions and 1665 deletions
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
|
@ -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' />}
|
||||
|
|
|
@ -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 };
|
|
@ -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;
|
|
@ -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;
|
|
@ -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';
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import { NKeyStorage } from './NKeyStorage';
|
||||
|
||||
export const NKeys = new NKeyStorage(
|
||||
localStorage,
|
||||
'soapbox:nostr:keys',
|
||||
);
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
|
|
|
@ -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' />
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 };
|
|
@ -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>;
|
||||
|
|
7
src/types/window.d.ts
vendored
7
src/types/window.d.ts
vendored
|
@ -1,7 +0,0 @@
|
|||
import type { NostrSigner } from '@soapbox/nspec';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
nostr?: NostrSigner;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
]),
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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 };
|
|
@ -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
174
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue