Merge branch 'main' into feat-choose-zaps-amount
(((Updating local branch)))
This commit is contained in:
commit
f7afa31fb8
|
@ -186,7 +186,6 @@
|
||||||
"vite-plugin-html": "^3.2.0",
|
"vite-plugin-html": "^3.2.0",
|
||||||
"vite-plugin-require": "^1.1.10",
|
"vite-plugin-require": "^1.1.10",
|
||||||
"vite-plugin-static-copy": "^1.0.0",
|
"vite-plugin-static-copy": "^1.0.0",
|
||||||
"websocket-ts": "^2.1.5",
|
|
||||||
"wicg-inert": "^3.1.1",
|
"wicg-inert": "^3.1.1",
|
||||||
"zod": "^3.23.5"
|
"zod": "^3.23.5"
|
||||||
},
|
},
|
||||||
|
|
|
@ -224,6 +224,11 @@ export const logOut = () =>
|
||||||
// Clear the account from Sentry.
|
// Clear the account from Sentry.
|
||||||
unsetSentryAccount();
|
unsetSentryAccount();
|
||||||
|
|
||||||
|
// Remove external auth entries.
|
||||||
|
localStorage.removeItem('soapbox:external:app');
|
||||||
|
localStorage.removeItem('soapbox:external:baseurl');
|
||||||
|
localStorage.removeItem('soapbox:external:scopes');
|
||||||
|
|
||||||
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
|
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
|
||||||
|
|
||||||
toast.success(messages.loggedOut);
|
toast.success(messages.loggedOut);
|
||||||
|
|
|
@ -99,5 +99,6 @@ export const loginWithCode = (code: string) =>
|
||||||
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)))
|
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)))
|
||||||
.then(({ access_token }: any) => dispatch(verifyCredentials(access_token as string, baseURL)))
|
.then(({ access_token }: any) => dispatch(verifyCredentials(access_token as string, baseURL)))
|
||||||
.then((account: { id: string }) => dispatch(switchAccount(account.id)))
|
.then((account: { id: string }) => dispatch(switchAccount(account.id)))
|
||||||
|
.then(() => localStorage.removeItem('soapbox:external:baseurl'))
|
||||||
.then(() => window.location.href = '/');
|
.then(() => window.location.href = '/');
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: A
|
||||||
id: accountId,
|
id: accountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
api(getState).get(`/api/v1/accounts/familiar_followers?id=${accountId}`)
|
api(getState).get(`/api/v1/accounts/familiar_followers?id[]=${accountId}`)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
const accounts = data.find(({ id }: { id: string }) => id === accountId).accounts;
|
const accounts = data.find(({ id }: { id: string }) => id === accountId).accounts;
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,31 @@
|
||||||
import { nip19 } from 'nostr-tools';
|
import { RootState, type AppDispatch } from 'soapbox/store';
|
||||||
|
|
||||||
import { type AppDispatch } from 'soapbox/store';
|
import { authLoggedIn, verifyCredentials } from './auth';
|
||||||
|
import { obtainOAuthToken } from './oauth';
|
||||||
|
|
||||||
import { verifyCredentials } from './auth';
|
const NOSTR_PUBKEY_SET = 'NOSTR_PUBKEY_SET';
|
||||||
|
|
||||||
/** Log in with a Nostr pubkey. */
|
/** Log in with a Nostr pubkey. */
|
||||||
function logInNostr(pubkey: string) {
|
function logInNostr(pubkey: string) {
|
||||||
return (dispatch: AppDispatch) => {
|
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const npub = nip19.npubEncode(pubkey);
|
dispatch(setNostrPubkey(pubkey));
|
||||||
return dispatch(verifyCredentials(npub));
|
|
||||||
|
const secret = sessionStorage.getItem('soapbox:nip46:secret');
|
||||||
|
if (!secret) {
|
||||||
|
throw new Error('No secret found in session storage');
|
||||||
|
}
|
||||||
|
|
||||||
|
const relay = getState().instance.nostr?.relay;
|
||||||
|
|
||||||
|
const token = await dispatch(obtainOAuthToken({
|
||||||
|
grant_type: 'nostr_bunker',
|
||||||
|
pubkey,
|
||||||
|
relays: relay ? [relay] : undefined,
|
||||||
|
secret,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { access_token } = dispatch(authLoggedIn(token));
|
||||||
|
return await dispatch(verifyCredentials(access_token as string));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,4 +40,11 @@ function nostrExtensionLogIn() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { logInNostr, nostrExtensionLogIn };
|
function setNostrPubkey(pubkey: string) {
|
||||||
|
return {
|
||||||
|
type: NOSTR_PUBKEY_SET,
|
||||||
|
pubkey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { logInNostr, nostrExtensionLogIn, setNostrPubkey, NOSTR_PUBKEY_SET };
|
|
@ -20,7 +20,7 @@ export const OAUTH_TOKEN_REVOKE_REQUEST = 'OAUTH_TOKEN_REVOKE_REQUEST';
|
||||||
export const OAUTH_TOKEN_REVOKE_SUCCESS = 'OAUTH_TOKEN_REVOKE_SUCCESS';
|
export const OAUTH_TOKEN_REVOKE_SUCCESS = 'OAUTH_TOKEN_REVOKE_SUCCESS';
|
||||||
export const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL';
|
export const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL';
|
||||||
|
|
||||||
export const obtainOAuthToken = (params: Record<string, string | undefined>, baseURL?: string) =>
|
export const obtainOAuthToken = (params: Record<string, unknown>, baseURL?: string) =>
|
||||||
(dispatch: AppDispatch) => {
|
(dispatch: AppDispatch) => {
|
||||||
dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params });
|
dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params });
|
||||||
return baseClient(null, baseURL).post('/oauth/token', params).then(({ data: token }) => {
|
return baseClient(null, baseURL).post('/oauth/token', params).then(({ data: token }) => {
|
||||||
|
|
|
@ -66,7 +66,7 @@ const fetchSoapboxConfig = (host: string | null) =>
|
||||||
dispatch(importSoapboxConfig(data.soapbox_fe, host));
|
dispatch(importSoapboxConfig(data.soapbox_fe, host));
|
||||||
return data.soapbox_fe;
|
return data.soapbox_fe;
|
||||||
} else {
|
} else {
|
||||||
return dispatch(fetchSoapboxJson(host));
|
return dispatch(soapboxConfigFail(new Error('Not found'), host));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||||
import { useApi } from 'soapbox/hooks/useApi';
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
|
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig';
|
||||||
import { type PatronUser, patronUserSchema } from 'soapbox/schemas';
|
import { type PatronUser, patronUserSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
function usePatronUser(url?: string) {
|
function usePatronUser(url?: string) {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
const soapboxConfig = useSoapboxConfig();
|
||||||
|
|
||||||
|
const patronEnabled = soapboxConfig.getIn(['extensions', 'patron', 'enabled']) === true;
|
||||||
|
|
||||||
const { entity: patronUser, ...result } = useEntity<PatronUser>(
|
const { entity: patronUser, ...result } = useEntity<PatronUser>(
|
||||||
[Entities.PATRON_USERS, url || ''],
|
[Entities.PATRON_USERS, url || ''],
|
||||||
() => api.get(`/api/patron/v1/accounts/${encodeURIComponent(url!)}`),
|
() => api.get(`/api/patron/v1/accounts/${encodeURIComponent(url!)}`),
|
||||||
{ schema: patronUserSchema, enabled: !!url },
|
{ schema: patronUserSchema, enabled: patronEnabled && !!url },
|
||||||
);
|
);
|
||||||
|
|
||||||
return { patronUser, ...result };
|
return { patronUser, ...result };
|
||||||
|
|
|
@ -1,146 +1,43 @@
|
||||||
import { NostrEvent, NostrConnectResponse, NSchema as n } from '@nostrify/nostrify';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { WebsocketEvent } from 'websocket-ts';
|
|
||||||
|
|
||||||
import { useNostr } from 'soapbox/contexts/nostr-context';
|
import { useNostr } from 'soapbox/contexts/nostr-context';
|
||||||
|
import { NConnect } from 'soapbox/features/nostr/NConnect';
|
||||||
|
|
||||||
|
const secretStorageKey = 'soapbox:nip46:secret';
|
||||||
|
|
||||||
|
sessionStorage.setItem(secretStorageKey, crypto.randomUUID());
|
||||||
|
|
||||||
function useSignerStream() {
|
function useSignerStream() {
|
||||||
const { relay, pubkey, signer } = useNostr();
|
const { relay, signer } = useNostr();
|
||||||
|
const [pubkey, setPubkey] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const [opened, setOpened] = useState(false);
|
const authStorageKey = `soapbox:nostr:auth:${pubkey}`;
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
|
||||||
|
|
||||||
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 = n.json().pipe(n.connectRequest()).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 handleEvent(event: NostrEvent) {
|
|
||||||
switch (event.kind) {
|
|
||||||
case 24133:
|
|
||||||
await handleConnectEvent(event);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (relay?.socket.readyState === WebSocket.OPEN) {
|
if (signer) {
|
||||||
setOpened(true);
|
signer.getPublicKey().then(setPubkey).catch(console.warn);
|
||||||
}
|
}
|
||||||
|
}, [signer]);
|
||||||
const openHandler = () => {
|
|
||||||
setOpened(true);
|
|
||||||
setIsConnected(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeHandler = () => {
|
|
||||||
setIsConnected(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
relay?.socket.addEventListener(WebsocketEvent.open, openHandler);
|
|
||||||
relay?.socket.addEventListener(WebsocketEvent.close, closeHandler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
relay?.socket.removeEventListener(WebsocketEvent.open, openHandler);
|
|
||||||
relay?.socket.removeEventListener(WebsocketEvent.close, closeHandler);
|
|
||||||
};
|
|
||||||
}, [relay]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!relay || !pubkey) return;
|
if (!relay || !signer || !pubkey) return;
|
||||||
|
|
||||||
const controller = new AbortController();
|
const connect = new NConnect({
|
||||||
const signal = controller.signal;
|
relay,
|
||||||
|
signer,
|
||||||
(async() => {
|
onAuthorize(authorizedPubkey) {
|
||||||
for await (const msg of relay.req([{ kinds: [24133], authors: [pubkey], limit: 0 }], { signal })) {
|
localStorage.setItem(authStorageKey, authorizedPubkey);
|
||||||
if (msg[0] === 'EVENT') handleEvent(msg[2]);
|
sessionStorage.setItem(secretStorageKey, crypto.randomUUID());
|
||||||
}
|
},
|
||||||
})();
|
authorizedPubkey: localStorage.getItem(authStorageKey) ?? undefined,
|
||||||
|
getSecret: () => sessionStorage.getItem(secretStorageKey)!,
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
controller.abort();
|
connect.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
}, [relay, pubkey, signer]);
|
}, [relay, signer, pubkey]);
|
||||||
|
|
||||||
return { opened, isConnected };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useSignerStream };
|
export { useSignerStream };
|
||||||
|
|
|
@ -103,7 +103,7 @@ export default (getState: () => RootState, authType: string = 'user'): AxiosInst
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const accessToken = getToken(state, authType);
|
const accessToken = getToken(state, authType);
|
||||||
const me = state.me;
|
const me = state.me;
|
||||||
const baseURL = me ? getAuthBaseURL(state, me) : localStorage.getItem('soapbox:external:baseurl') ?? '';
|
const baseURL = me ? getAuthBaseURL(state, me) : '';
|
||||||
|
|
||||||
const relayUrl = state.getIn(['instance', 'nostr', 'relay']) as string | undefined;
|
const relayUrl = state.getIn(['instance', 'nostr', 'relay']) as string | undefined;
|
||||||
const pubkey = state.getIn(['instance', 'nostr', 'pubkey']) as string | undefined;
|
const pubkey = state.getIn(['instance', 'nostr', 'pubkey']) as string | undefined;
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { useAccount } from 'soapbox/api/hooks';
|
||||||
import Account from 'soapbox/components/account';
|
import Account from 'soapbox/components/account';
|
||||||
import { Stack, Divider, HStack, Icon, IconButton, Text } from 'soapbox/components/ui';
|
import { Stack, Divider, HStack, Icon, IconButton, Text } from 'soapbox/components/ui';
|
||||||
import ProfileStats from 'soapbox/features/ui/components/profile-stats';
|
import ProfileStats from 'soapbox/features/ui/components/profile-stats';
|
||||||
import { useAppDispatch, useAppSelector, useGroupsPath, useFeatures, useInstance } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useFeatures, useInstance } from 'soapbox/hooks';
|
||||||
import { makeGetOtherAccounts } from 'soapbox/selectors';
|
import { makeGetOtherAccounts } from 'soapbox/selectors';
|
||||||
|
|
||||||
import type { List as ImmutableList } from 'immutable';
|
import type { List as ImmutableList } from 'immutable';
|
||||||
|
@ -88,7 +88,6 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||||
const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen);
|
const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen);
|
||||||
const settings = useAppSelector((state) => getSettings(state));
|
const settings = useAppSelector((state) => getSettings(state));
|
||||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||||
const groupsPath = useGroupsPath();
|
|
||||||
const instance = useInstance();
|
const instance = useInstance();
|
||||||
|
|
||||||
const closeButtonRef = React.useRef(null);
|
const closeButtonRef = React.useRef(null);
|
||||||
|
@ -210,7 +209,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||||
|
|
||||||
{features.groups && (
|
{features.groups && (
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
to={groupsPath}
|
to='/groups'
|
||||||
icon={require('@tabler/icons/outline/circles.svg')}
|
icon={require('@tabler/icons/outline/circles.svg')}
|
||||||
text={intl.formatMessage(messages.groups)}
|
text={intl.formatMessage(messages.groups)}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { Stack } from 'soapbox/components/ui';
|
import { Stack } from 'soapbox/components/ui';
|
||||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||||
import ComposeButton from 'soapbox/features/ui/components/compose-button';
|
import ComposeButton from 'soapbox/features/ui/components/compose-button';
|
||||||
import { useAppSelector, useGroupsPath, useFeatures, useOwnAccount, useSettings, useInstance } from 'soapbox/hooks';
|
import { useAppSelector, useFeatures, useOwnAccount, useSettings, useInstance } from 'soapbox/hooks';
|
||||||
|
|
||||||
import DropdownMenu, { Menu } from './dropdown-menu';
|
import DropdownMenu, { Menu } from './dropdown-menu';
|
||||||
import SidebarNavigationLink from './sidebar-navigation-link';
|
import SidebarNavigationLink from './sidebar-navigation-link';
|
||||||
|
@ -26,7 +26,6 @@ const SidebarNavigation = () => {
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const { isDeveloper } = useSettings();
|
const { isDeveloper } = useSettings();
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
const groupsPath = useGroupsPath();
|
|
||||||
|
|
||||||
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
||||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||||
|
@ -142,7 +141,7 @@ const SidebarNavigation = () => {
|
||||||
|
|
||||||
{features.groups && (
|
{features.groups && (
|
||||||
<SidebarNavigationLink
|
<SidebarNavigationLink
|
||||||
to={groupsPath}
|
to='/groups'
|
||||||
icon={require('@tabler/icons/outline/circles.svg')}
|
icon={require('@tabler/icons/outline/circles.svg')}
|
||||||
activeIcon={require('@tabler/icons/filled/circles.svg')}
|
activeIcon={require('@tabler/icons/filled/circles.svg')}
|
||||||
text={<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />}
|
text={<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />}
|
||||||
|
|
|
@ -3,12 +3,11 @@ import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import ThumbNavigationLink from 'soapbox/components/thumb-navigation-link';
|
import ThumbNavigationLink from 'soapbox/components/thumb-navigation-link';
|
||||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||||
import { useAppSelector, useFeatures, useGroupsPath, useOwnAccount } from 'soapbox/hooks';
|
import { useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
|
||||||
|
|
||||||
const ThumbNavigation: React.FC = (): JSX.Element => {
|
const ThumbNavigation: React.FC = (): JSX.Element => {
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const groupsPath = useGroupsPath();
|
|
||||||
|
|
||||||
const { unreadChatsCount } = useStatContext();
|
const { unreadChatsCount } = useStatContext();
|
||||||
|
|
||||||
|
@ -60,7 +59,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
|
||||||
src={require('@tabler/icons/outline/circles.svg')}
|
src={require('@tabler/icons/outline/circles.svg')}
|
||||||
activeSrc={require('@tabler/icons/filled/circles.svg')}
|
activeSrc={require('@tabler/icons/filled/circles.svg')}
|
||||||
text={<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />}
|
text={<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />}
|
||||||
to={groupsPath}
|
to='/groups'
|
||||||
exact
|
exact
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { NRelay1, NostrSigner } from '@nostrify/nostrify';
|
import { NRelay, NRelay1, NostrSigner } from '@nostrify/nostrify';
|
||||||
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { NKeys } from 'soapbox/features/nostr/keys';
|
import { NKeys } from 'soapbox/features/nostr/keys';
|
||||||
import { useOwnAccount } from 'soapbox/hooks';
|
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
|
||||||
import { useInstance } from 'soapbox/hooks/useInstance';
|
import { useInstance } from 'soapbox/hooks/useInstance';
|
||||||
|
|
||||||
interface NostrContextType {
|
interface NostrContextType {
|
||||||
relay?: NRelay1;
|
relay?: NRelay;
|
||||||
pubkey?: string;
|
|
||||||
signer?: NostrSigner;
|
signer?: NostrSigner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,8 +23,7 @@ export const NostrProvider: React.FC<NostrProviderProps> = ({ children }) => {
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
|
|
||||||
const url = instance.nostr?.relay;
|
const url = instance.nostr?.relay;
|
||||||
const pubkey = instance.nostr?.pubkey;
|
const accountPubkey = useAppSelector((state) => account?.nostr.pubkey ?? state.meta.pubkey);
|
||||||
const accountPubkey = account?.nostr.pubkey;
|
|
||||||
|
|
||||||
const signer = useMemo(
|
const signer = useMemo(
|
||||||
() => (accountPubkey ? NKeys.get(accountPubkey) : undefined) ?? window.nostr,
|
() => (accountPubkey ? NKeys.get(accountPubkey) : undefined) ?? window.nostr,
|
||||||
|
@ -42,7 +40,7 @@ export const NostrProvider: React.FC<NostrProviderProps> = ({ children }) => {
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NostrContext.Provider value={{ relay, pubkey, signer }}>
|
<NostrContext.Provider value={{ relay, signer }}>
|
||||||
{children}
|
{children}
|
||||||
</NostrContext.Provider>
|
</NostrContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -464,7 +464,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!account.local) {
|
if (!account.local && features.domainBlocks) {
|
||||||
const domain = account.fqn.split('@')[1];
|
const domain = account.fqn.split('@')[1];
|
||||||
|
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { openModal } from 'soapbox/actions/modals';
|
||||||
import { useDeleteGroup, useGroup } from 'soapbox/api/hooks';
|
import { useDeleteGroup, useGroup } from 'soapbox/api/hooks';
|
||||||
import List, { ListItem } from 'soapbox/components/list';
|
import List, { ListItem } from 'soapbox/components/list';
|
||||||
import { CardBody, CardHeader, CardTitle, Column, Spinner, Text } from 'soapbox/components/ui';
|
import { CardBody, CardHeader, CardTitle, Column, Spinner, Text } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useBackend, useGroupsPath } from 'soapbox/hooks';
|
import { useAppDispatch, useBackend } from 'soapbox/hooks';
|
||||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
import { TRUTHSOCIAL } from 'soapbox/utils/features';
|
import { TRUTHSOCIAL } from 'soapbox/utils/features';
|
||||||
|
@ -38,7 +38,6 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
|
||||||
|
|
||||||
const backend = useBackend();
|
const backend = useBackend();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const groupsPath = useGroupsPath();
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
@ -70,7 +69,7 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
|
||||||
deleteGroup.mutate(group.id, {
|
deleteGroup.mutate(group.id, {
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
toast.success(intl.formatMessage(messages.deleteSuccess));
|
toast.success(intl.formatMessage(messages.deleteSuccess));
|
||||||
history.push(groupsPath);
|
history.push('/groups');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { NRelay, NostrConnectRequest, NostrConnectResponse, NostrEvent, NostrSigner, NSchema as n } from '@nostrify/nostrify';
|
||||||
|
|
||||||
|
interface NConnectOpts {
|
||||||
|
relay: NRelay;
|
||||||
|
signer: NostrSigner;
|
||||||
|
authorizedPubkey: string | undefined;
|
||||||
|
onAuthorize(pubkey: string): void;
|
||||||
|
getSecret(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NConnect {
|
||||||
|
|
||||||
|
private relay: NRelay;
|
||||||
|
private signer: NostrSigner;
|
||||||
|
private authorizedPubkey: string | undefined;
|
||||||
|
private onAuthorize: (pubkey: string) => void;
|
||||||
|
private getSecret: () => string;
|
||||||
|
|
||||||
|
private controller = new AbortController();
|
||||||
|
|
||||||
|
constructor(opts: NConnectOpts) {
|
||||||
|
this.relay = opts.relay;
|
||||||
|
this.signer = opts.signer;
|
||||||
|
this.authorizedPubkey = opts.authorizedPubkey;
|
||||||
|
this.onAuthorize = opts.onAuthorize;
|
||||||
|
this.getSecret = opts.getSecret;
|
||||||
|
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async open() {
|
||||||
|
const pubkey = await this.signer.getPublicKey();
|
||||||
|
const signal = this.controller.signal;
|
||||||
|
|
||||||
|
for await (const msg of this.relay.req([{ kinds: [24133], '#p': [pubkey], limit: 0 }], { signal })) {
|
||||||
|
if (msg[0] === 'EVENT') {
|
||||||
|
const event = msg[2];
|
||||||
|
this.handleEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleEvent(event: NostrEvent): Promise<void> {
|
||||||
|
const decrypted = await this.signer.nip04!.decrypt(event.pubkey, event.content);
|
||||||
|
const request = n.json().pipe(n.connectRequest()).safeParse(decrypted);
|
||||||
|
|
||||||
|
if (!request.success) {
|
||||||
|
console.warn(decrypted);
|
||||||
|
console.warn(request.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.handleRequest(event.pubkey, request.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRequest(pubkey: string, request: NostrConnectRequest): Promise<void> {
|
||||||
|
// Connect is a special case. Any pubkey can try to request it.
|
||||||
|
if (request.method === 'connect') {
|
||||||
|
return this.handleConnect(pubkey, request as NostrConnectRequest & { method: 'connect' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent unauthorized access.
|
||||||
|
if (pubkey !== this.authorizedPubkey) {
|
||||||
|
return this.sendResponse(pubkey, {
|
||||||
|
id: request.id,
|
||||||
|
result: '',
|
||||||
|
error: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorized methods.
|
||||||
|
switch (request.method) {
|
||||||
|
case 'sign_event':
|
||||||
|
return this.sendResponse(pubkey, {
|
||||||
|
id: request.id,
|
||||||
|
result: JSON.stringify(await this.signer.signEvent(JSON.parse(request.params[0]))),
|
||||||
|
});
|
||||||
|
case 'ping':
|
||||||
|
return this.sendResponse(pubkey, {
|
||||||
|
id: request.id,
|
||||||
|
result: 'pong',
|
||||||
|
});
|
||||||
|
case 'get_relays':
|
||||||
|
return this.sendResponse(pubkey, {
|
||||||
|
id: request.id,
|
||||||
|
result: JSON.stringify(await this.signer.getRelays?.() ?? []),
|
||||||
|
});
|
||||||
|
case 'get_public_key':
|
||||||
|
return this.sendResponse(pubkey, {
|
||||||
|
id: request.id,
|
||||||
|
result: await this.signer.getPublicKey(),
|
||||||
|
});
|
||||||
|
case 'nip04_encrypt':
|
||||||
|
return this.sendResponse(pubkey, {
|
||||||
|
id: request.id,
|
||||||
|
result: await this.signer.nip04!.encrypt(request.params[0], request.params[1]),
|
||||||
|
});
|
||||||
|
case 'nip04_decrypt':
|
||||||
|
return this.sendResponse(pubkey, {
|
||||||
|
id: request.id,
|
||||||
|
result: await this.signer.nip04!.decrypt(request.params[0], request.params[1]),
|
||||||
|
});
|
||||||
|
case 'nip44_encrypt':
|
||||||
|
return this.sendResponse(pubkey, {
|
||||||
|
id: request.id,
|
||||||
|
result: await this.signer.nip44!.encrypt(request.params[0], request.params[1]),
|
||||||
|
});
|
||||||
|
case 'nip44_decrypt':
|
||||||
|
return this.sendResponse(pubkey, {
|
||||||
|
id: request.id,
|
||||||
|
result: await this.signer.nip44!.decrypt(request.params[0], request.params[1]),
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return this.sendResponse(pubkey, {
|
||||||
|
id: request.id,
|
||||||
|
result: '',
|
||||||
|
error: `Unrecognized method: ${request.method}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleConnect(pubkey: string, request: NostrConnectRequest & { method: 'connect' }) {
|
||||||
|
const [remotePubkey, secret] = request.params;
|
||||||
|
|
||||||
|
if (secret === this.getSecret() && remotePubkey === await this.signer.getPublicKey()) {
|
||||||
|
this.authorizedPubkey = pubkey;
|
||||||
|
this.onAuthorize(pubkey);
|
||||||
|
|
||||||
|
await this.sendResponse(pubkey, {
|
||||||
|
id: request.id,
|
||||||
|
result: 'ack',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendResponse(pubkey: string, response: NostrConnectResponse) {
|
||||||
|
const event = await this.signer.signEvent({
|
||||||
|
kind: 24133,
|
||||||
|
content: await this.signer.nip04!.encrypt(pubkey, JSON.stringify(response)),
|
||||||
|
tags: [['p', pubkey]],
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.relay.event(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.controller.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -53,7 +53,7 @@ const LinkFooter: React.FC = (): JSX.Element => {
|
||||||
{features.followedHashtagsList && (
|
{features.followedHashtagsList && (
|
||||||
<FooterLink to='/followed_tags'><FormattedMessage id='navigation_bar.followed_tags' defaultMessage='Followed hashtags' /></FooterLink>
|
<FooterLink to='/followed_tags'><FormattedMessage id='navigation_bar.followed_tags' defaultMessage='Followed hashtags' /></FooterLink>
|
||||||
)}
|
)}
|
||||||
{features.federating && (
|
{features.domainBlocks && (
|
||||||
<FooterLink to='/domain_blocks'><FormattedMessage id='navigation_bar.domain_blocks' defaultMessage='Domain blocks' /></FooterLink>
|
<FooterLink to='/domain_blocks'><FormattedMessage id='navigation_bar.domain_blocks' defaultMessage='Domain blocks' /></FooterLink>
|
||||||
)}
|
)}
|
||||||
{account.admin && (
|
{account.admin && (
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses';
|
||||||
import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions';
|
import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions';
|
||||||
import { expandHomeTimeline } from 'soapbox/actions/timelines';
|
import { expandHomeTimeline } from 'soapbox/actions/timelines';
|
||||||
import { useUserStream } from 'soapbox/api/hooks';
|
import { useUserStream } from 'soapbox/api/hooks';
|
||||||
|
import { useSignerStream } from 'soapbox/api/hooks/nostr/useSignerStream';
|
||||||
import SidebarNavigation from 'soapbox/components/sidebar-navigation';
|
import SidebarNavigation from 'soapbox/components/sidebar-navigation';
|
||||||
import ThumbNavigation from 'soapbox/components/thumb-navigation';
|
import ThumbNavigation from 'soapbox/components/thumb-navigation';
|
||||||
import { Layout } from 'soapbox/components/ui';
|
import { Layout } from 'soapbox/components/ui';
|
||||||
|
@ -268,7 +269,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
||||||
|
|
||||||
<WrappedRoute path='/follow_requests' page={DefaultPage} component={FollowRequests} content={children} />
|
<WrappedRoute path='/follow_requests' page={DefaultPage} component={FollowRequests} content={children} />
|
||||||
{features.blocks && <WrappedRoute path='/blocks' page={DefaultPage} component={Blocks} content={children} />}
|
{features.blocks && <WrappedRoute path='/blocks' page={DefaultPage} component={Blocks} content={children} />}
|
||||||
{features.federating && <WrappedRoute path='/domain_blocks' page={DefaultPage} component={DomainBlocks} content={children} />}
|
{features.domainBlocks && <WrappedRoute path='/domain_blocks' page={DefaultPage} component={DomainBlocks} content={children} />}
|
||||||
<WrappedRoute path='/mutes' page={DefaultPage} component={Mutes} content={children} />
|
<WrappedRoute path='/mutes' page={DefaultPage} component={Mutes} content={children} />
|
||||||
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters/new' page={DefaultPage} component={EditFilter} content={children} />}
|
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters/new' page={DefaultPage} component={EditFilter} content={children} />}
|
||||||
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters/:id' page={DefaultPage} component={EditFilter} content={children} />}
|
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters/:id' page={DefaultPage} component={EditFilter} content={children} />}
|
||||||
|
@ -461,6 +462,7 @@ const UI: React.FC<IUI> = ({ children }) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useUserStream();
|
useUserStream();
|
||||||
|
useSignerStream();
|
||||||
|
|
||||||
// The user has logged in
|
// The user has logged in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -7,7 +7,6 @@ export { useCompose } from './useCompose';
|
||||||
export { useDebounce } from './useDebounce';
|
export { useDebounce } from './useDebounce';
|
||||||
export { useDraggedFiles } from './useDraggedFiles';
|
export { useDraggedFiles } from './useDraggedFiles';
|
||||||
export { useGetState } from './useGetState';
|
export { useGetState } from './useGetState';
|
||||||
export { useGroupsPath } from './useGroupsPath';
|
|
||||||
export { useDimensions } from './useDimensions';
|
export { useDimensions } from './useDimensions';
|
||||||
export { useFeatures } from './useFeatures';
|
export { useFeatures } from './useFeatures';
|
||||||
export { useInstance } from './useInstance';
|
export { useInstance } from './useInstance';
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
import { __stub } from 'soapbox/api';
|
|
||||||
import { buildAccount, buildGroup, buildGroupRelationship } from 'soapbox/jest/factory';
|
|
||||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
|
||||||
import { instanceSchema } from 'soapbox/schemas';
|
|
||||||
|
|
||||||
import { useGroupsPath } from './useGroupsPath';
|
|
||||||
|
|
||||||
describe('useGroupsPath()', () => {
|
|
||||||
test('without the groupsDiscovery feature', () => {
|
|
||||||
const store = {
|
|
||||||
instance: instanceSchema.parse({
|
|
||||||
version: '2.7.2 (compatible; Pleroma 2.3.0)',
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const { result } = renderHook(useGroupsPath, undefined, store);
|
|
||||||
|
|
||||||
expect(result.current).toEqual('/groups');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with the "groupsDiscovery" feature', () => {
|
|
||||||
let store: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const userId = '1';
|
|
||||||
store = {
|
|
||||||
instance: instanceSchema.parse({
|
|
||||||
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
|
||||||
}),
|
|
||||||
me: userId,
|
|
||||||
accounts: {
|
|
||||||
[userId]: buildAccount({
|
|
||||||
id: userId,
|
|
||||||
acct: 'justin-username',
|
|
||||||
display_name: 'Justin L',
|
|
||||||
avatar: 'test.jpg',
|
|
||||||
source: {
|
|
||||||
chats_onboarded: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the user has no groups', () => {
|
|
||||||
test('should default to the discovery page', () => {
|
|
||||||
const { result } = renderHook(useGroupsPath, undefined, store);
|
|
||||||
|
|
||||||
expect(result.current).toEqual('/groups/discover');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the user has groups', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet('/api/v1/groups').reply(200, [
|
|
||||||
buildGroup({
|
|
||||||
display_name: 'Group',
|
|
||||||
id: '1',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
mock.onGet('/api/v1/groups/relationships?id[]=1').reply(200, [
|
|
||||||
buildGroupRelationship({
|
|
||||||
id: '1',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should default to the "My Groups" page', async () => {
|
|
||||||
const { result } = renderHook(useGroupsPath, undefined, store);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current).toEqual('/groups');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { useGroups } from 'soapbox/api/hooks/groups/useGroups';
|
|
||||||
|
|
||||||
import { useFeatures } from './useFeatures';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the correct URL to use for /groups.
|
|
||||||
* If the user does not have any Groups, let's default to the discovery tab.
|
|
||||||
* Otherwise, let's default to My Groups.
|
|
||||||
*
|
|
||||||
* @returns String (as link)
|
|
||||||
*/
|
|
||||||
const useGroupsPath = () => {
|
|
||||||
const features = useFeatures();
|
|
||||||
const { groups } = useGroups();
|
|
||||||
|
|
||||||
if (!features.groupsDiscovery) {
|
|
||||||
return '/groups';
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups.length > 0 ? '/groups' : '/groups/discover';
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useGroupsPath };
|
|
|
@ -1,34 +0,0 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { fetchInstance } from 'soapbox/actions/instance';
|
|
||||||
import { useSignerStream } from 'soapbox/api/hooks/nostr/useSignerStream';
|
|
||||||
import LoadingScreen from 'soapbox/components/loading-screen';
|
|
||||||
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
|
||||||
|
|
||||||
interface ISoapboxInstance {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SoapboxInstance: React.FC<ISoapboxInstance> = ({ children }) => {
|
|
||||||
const features = useFeatures();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
|
||||||
const { opened } = useSignerStream();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchInstance()).then(() => {
|
|
||||||
setIsLoaded(true);
|
|
||||||
}).catch(() => {
|
|
||||||
setIsLoaded(true);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!isLoaded || (!opened && features.nostr)) {
|
|
||||||
return <LoadingScreen />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SoapboxInstance;
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
|
import { fetchInstance } from 'soapbox/actions/instance';
|
||||||
import { fetchMe } from 'soapbox/actions/me';
|
import { fetchMe } from 'soapbox/actions/me';
|
||||||
import { loadSoapboxConfig } from 'soapbox/actions/soapbox';
|
import { loadSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
import LoadingScreen from 'soapbox/components/loading-screen';
|
import LoadingScreen from 'soapbox/components/loading-screen';
|
||||||
|
@ -16,7 +17,11 @@ import MESSAGES from 'soapbox/messages';
|
||||||
const loadInitial = () => {
|
const loadInitial = () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return async(dispatch, getState) => {
|
return async(dispatch, getState) => {
|
||||||
|
// Await for authenticated fetch
|
||||||
await dispatch(fetchMe());
|
await dispatch(fetchMe());
|
||||||
|
// Await for feature detection
|
||||||
|
await dispatch(fetchInstance());
|
||||||
|
// Await for configuration
|
||||||
await dispatch(loadSoapboxConfig());
|
await dispatch(loadSoapboxConfig());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { preload } from '../actions/preload';
|
||||||
import { store } from '../store';
|
import { store } from '../store';
|
||||||
|
|
||||||
import SoapboxHead from './soapbox-head';
|
import SoapboxHead from './soapbox-head';
|
||||||
import SoapboxInstance from './soapbox-instance';
|
|
||||||
import SoapboxLoad from './soapbox-load';
|
import SoapboxLoad from './soapbox-load';
|
||||||
import SoapboxMount from './soapbox-mount';
|
import SoapboxMount from './soapbox-mount';
|
||||||
|
|
||||||
|
@ -32,13 +31,11 @@ const Soapbox: React.FC = () => {
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<StatProvider>
|
<StatProvider>
|
||||||
<NostrProvider>
|
<NostrProvider>
|
||||||
<SoapboxInstance>
|
|
||||||
<SoapboxHead>
|
<SoapboxHead>
|
||||||
<SoapboxLoad>
|
<SoapboxLoad>
|
||||||
<SoapboxMount />
|
<SoapboxMount />
|
||||||
</SoapboxLoad>
|
</SoapboxLoad>
|
||||||
</SoapboxHead>
|
</SoapboxHead>
|
||||||
</SoapboxInstance>
|
|
||||||
</NostrProvider>
|
</NostrProvider>
|
||||||
</StatProvider>
|
</StatProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Record as ImmutableRecord } from 'immutable';
|
import { Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
import { fetchInstance } from 'soapbox/actions/instance';
|
import { fetchInstance } from 'soapbox/actions/instance';
|
||||||
|
import { NOSTR_PUBKEY_SET } from 'soapbox/actions/nostr';
|
||||||
import { SW_UPDATING } from 'soapbox/actions/sw';
|
import { SW_UPDATING } from 'soapbox/actions/sw';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
|
@ -10,6 +11,8 @@ const ReducerRecord = ImmutableRecord({
|
||||||
instance_fetch_failed: false,
|
instance_fetch_failed: false,
|
||||||
/** Whether the ServiceWorker is currently updating (and we should display a loading screen). */
|
/** Whether the ServiceWorker is currently updating (and we should display a loading screen). */
|
||||||
swUpdating: false,
|
swUpdating: false,
|
||||||
|
/** User's nostr pubkey. */
|
||||||
|
pubkey: undefined as string | undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function meta(state = ReducerRecord(), action: AnyAction) {
|
export default function meta(state = ReducerRecord(), action: AnyAction) {
|
||||||
|
@ -21,6 +24,8 @@ export default function meta(state = ReducerRecord(), action: AnyAction) {
|
||||||
return state;
|
return state;
|
||||||
case SW_UPDATING:
|
case SW_UPDATING:
|
||||||
return state.set('swUpdating', action.isUpdating);
|
return state.set('swUpdating', action.isUpdating);
|
||||||
|
case NOSTR_PUBKEY_SET:
|
||||||
|
return state.set('pubkey', action.pubkey);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,7 @@
|
||||||
import { NSchema as n } from '@nostrify/nostrify';
|
import { NSchema as n } from '@nostrify/nostrify';
|
||||||
import { verifyEvent } from 'nostr-tools';
|
import { verifyEvent } from 'nostr-tools';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
/** Nostr event schema that also verifies the event's signature. */
|
/** Nostr event schema that also verifies the event's signature. */
|
||||||
const signedEventSchema = n.event().refine(verifyEvent);
|
const signedEventSchema = n.event().refine(verifyEvent);
|
||||||
|
|
||||||
/** NIP-47 signer response. */
|
export { signedEventSchema };
|
||||||
const nwcRequestSchema = z.object({
|
|
||||||
method: z.literal('pay_invoice'),
|
|
||||||
params: z.object({
|
|
||||||
invoice: z.string(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export { signedEventSchema, nwcRequestSchema };
|
|
|
@ -400,6 +400,17 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
dislikes: v.software === FRIENDICA && gte(v.version, '2023.3.0'),
|
dislikes: v.software === FRIENDICA && gte(v.version, '2023.3.0'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ability to block users by domain.
|
||||||
|
* @see GET /api/v1/domain_blocks
|
||||||
|
* @see POST /api/v1/domain_blocks
|
||||||
|
* @see DELETE /api/v1/domain_blocks
|
||||||
|
*/
|
||||||
|
domainBlocks: federation.enabled && any([
|
||||||
|
v.software === MASTODON && gte(v.compatVersion, '1.4.0'),
|
||||||
|
v.software === PLEROMA,
|
||||||
|
]),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow to register on a given domain
|
* Allow to register on a given domain
|
||||||
* @see GET /api/v1/pleroma/admin/domains
|
* @see GET /api/v1/pleroma/admin/domains
|
||||||
|
@ -529,6 +540,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
v.software === MASTODON && gte(v.version, '3.5.0'),
|
v.software === MASTODON && gte(v.version, '3.5.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '2.5.51') && v.build === REBASED,
|
v.software === PLEROMA && gte(v.version, '2.5.51') && v.build === REBASED,
|
||||||
v.software === TAKAHE,
|
v.software === TAKAHE,
|
||||||
|
v.software === DITTO,
|
||||||
]),
|
]),
|
||||||
|
|
||||||
/** Whether the instance federates. */
|
/** Whether the instance federates. */
|
||||||
|
|
Loading…
Reference in New Issue