Merge remote-tracking branch 'origin/main' into upgrade-deps

This commit is contained in:
Alex Gleason 2024-10-18 21:49:22 -05:00
commit 588b86eb84
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
25 changed files with 222 additions and 484 deletions

View File

@ -41,7 +41,7 @@
],
"dependencies": {
"@akryum/flexsearch-es": "^0.7.32",
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/data": "^1.2.1",
"@floating-ui/react": "^0.26.0",
"@fontsource/inter": "^5.0.0",
"@fontsource/noto-sans-javanese": "^5.0.16",
@ -72,6 +72,7 @@
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/react-query": "^5.59.13",
"@twemoji/svg": "^15.0.0",
"@types/escape-html": "^1.0.1",
"@types/http-link-header": "^1.0.3",
"@types/leaflet": "^1.8.0",
@ -102,8 +103,8 @@
"cryptocurrency-icons": "^0.18.1",
"cssnano": "^6.0.0",
"detect-passive-events": "^2.0.0",
"emoji-datasource": "14.0.0",
"emoji-mart": "^5.5.2",
"emoji-datasource": "15.0.1",
"emoji-mart": "^5.6.0",
"escape-html": "^1.0.3",
"eslint-plugin-formatjs": "^4.12.2",
"exifr": "^7.1.3",
@ -152,7 +153,6 @@
"sass": "^1.79.5",
"semver": "^7.3.8",
"stringz": "^2.0.0",
"twemoji": "https://github.com/twitter/twemoji#v14.0.2",
"type-fest": "^4.0.0",
"typescript": "^5.6.2",
"vite": "^5.4.8",

View File

@ -1,25 +0,0 @@
import { register, saveSettings } from './registerer';
import {
SET_BROWSER_SUPPORT,
SET_SUBSCRIPTION,
CLEAR_SUBSCRIPTION,
SET_ALERTS,
setAlerts,
} from './setter';
import type { AppDispatch } from 'soapbox/store';
export {
SET_BROWSER_SUPPORT,
SET_SUBSCRIPTION,
CLEAR_SUBSCRIPTION,
SET_ALERTS,
register,
changeAlerts,
};
const changeAlerts = (path: Array<string>, value: any) =>
(dispatch: AppDispatch) => {
dispatch(setAlerts(path, value));
dispatch(saveSettings() as any);
};

View File

@ -1,159 +1,108 @@
import { createPushSubscription, updatePushSubscription } from 'soapbox/actions/push-subscriptions';
import { pushNotificationsSetting } from 'soapbox/settings';
import { getVapidKey } from 'soapbox/utils/auth';
import { decode as decodeBase64 } from 'soapbox/utils/base64';
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { Me } from 'soapbox/types/soapbox';
// Taken from https://www.npmjs.com/package/web-push
const urlBase64ToUint8Array = (base64String: string) => {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
return decodeBase64(base64);
};
const getRegistration = () => {
if (navigator.serviceWorker) {
return navigator.serviceWorker.ready;
} else {
throw 'Your browser does not support Service Workers.';
}
};
const getPushSubscription = (registration: ServiceWorkerRegistration) =>
registration.pushManager.getSubscription()
.then(subscription => ({ registration, subscription }));
const subscribe = (registration: ServiceWorkerRegistration, getState: () => RootState) =>
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState())),
});
const unsubscribe = ({ registration, subscription }: {
registration: ServiceWorkerRegistration;
subscription: PushSubscription | null;
}) =>
subscription ? subscription.unsubscribe().then(() => registration) : new Promise<ServiceWorkerRegistration>(r => r(registration));
const sendSubscriptionToBackend = (subscription: PushSubscription, me: Me) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const alerts = getState().push_notifications.alerts.toJS();
const params = { subscription: subscription.toJSON(), data: { alerts } };
if (me) {
const data = pushNotificationsSetting.get(me);
if (data) {
params.data = data;
}
}
return dispatch(createPushSubscription(params));
};
/* eslint-disable compat/compat */
import { HTTPError } from 'soapbox/api/HTTPError';
import { MastodonClient } from 'soapbox/api/MastodonClient';
import { WebPushSubscription, webPushSubscriptionSchema } from 'soapbox/schemas/web-push';
import { decodeBase64Url } from 'soapbox/utils/base64';
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
// eslint-disable-next-line compat/compat
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
const register = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const me = getState().me;
const vapidKey = getVapidKey(getState());
/**
* Register web push notifications.
* This function creates a subscription if one hasn't been created already, and syncronizes it with the backend.
*/
export async function registerPushNotifications(api: MastodonClient, vapidKey: string) {
if (!supportsPushNotifications) {
console.warn('Your browser does not support Web Push Notifications.');
return;
}
dispatch(setBrowserSupport(supportsPushNotifications));
const { subscription, created } = await getOrCreateSubscription(vapidKey);
if (!supportsPushNotifications) {
console.warn('Your browser does not support Web Push Notifications.');
return;
if (created) {
await sendSubscriptionToBackend(api, subscription);
return;
}
// We have a subscription, check if it is still valid.
const backend = await getBackendSubscription(api);
// If the VAPID public key did not change and the endpoint corresponds
// to the endpoint saved in the backend, the subscription is valid.
if (backend && subscriptionMatchesBackend(subscription, backend)) {
return;
} else {
// Something went wrong, try to subscribe again.
await subscription.unsubscribe();
const newSubscription = await createSubscription(vapidKey);
await sendSubscriptionToBackend(api, newSubscription);
}
}
/** Get an existing subscription object from the browser if it exists, or ask the browser to create one. */
async function getOrCreateSubscription(vapidKey: string): Promise<{ subscription: PushSubscription; created: boolean }> {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
return { subscription, created: false };
} else {
const subscription = await createSubscription(vapidKey);
return { subscription, created: true };
}
}
/** Request a subscription object from the web browser. */
async function createSubscription(vapidKey: string): Promise<PushSubscription> {
const registration = await navigator.serviceWorker.ready;
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: decodeBase64Url(vapidKey),
});
}
/** Fetch the API for an existing subscription saved in the backend, if any. */
async function getBackendSubscription(api: MastodonClient): Promise<WebPushSubscription | null> {
try {
const response = await api.get('/api/v1/push/subscription');
const data = await response.json();
return webPushSubscriptionSchema.parse(data);
} catch (e) {
if (e instanceof HTTPError && e.response.status === 404) {
return null;
} else {
throw e;
}
}
}
if (!vapidKey) {
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
return;
}
getRegistration()
.then(getPushSubscription)
// @ts-ignore
.then(({ registration, subscription }: {
registration: ServiceWorkerRegistration;
subscription: PushSubscription | null;
}) => {
if (subscription !== null) {
// We have a subscription, check if it is still valid
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey!)).toString();
const subscriptionServerKey = urlBase64ToUint8Array(vapidKey).toString();
const serverEndpoint = getState().push_notifications.subscription?.endpoint;
// If the VAPID public key did not change and the endpoint corresponds
// to the endpoint saved in the backend, the subscription is valid
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
return { subscription };
} else {
// Something went wrong, try to subscribe again
return unsubscribe({ registration, subscription }).then((registration: ServiceWorkerRegistration) => {
return subscribe(registration, getState);
}).then(
(subscription: PushSubscription) => dispatch(sendSubscriptionToBackend(subscription, me) as any));
}
}
// No subscription, try to subscribe
return subscribe(registration, getState)
.then(subscription => dispatch(sendSubscriptionToBackend(subscription, me) as any));
})
.then(({ subscription }: { subscription: PushSubscription | Record<string, any> }) => {
// If we got a PushSubscription (and not a subscription object from the backend)
// it means that the backend subscription is valid (and was set during hydration)
if (!(subscription instanceof PushSubscription)) {
dispatch(setSubscription(subscription as PushSubscription));
if (me) {
pushNotificationsSetting.set(me, { alerts: subscription.alerts });
}
}
})
.catch(error => {
if (error.code === 20 && error.name === 'AbortError') {
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
console.error('The VAPID public key seems to be invalid:', vapidKey);
}
// Clear alerts and hide UI settings
dispatch(clearSubscription());
if (me) {
pushNotificationsSetting.remove(me);
}
return getRegistration()
.then(getPushSubscription)
.then(unsubscribe);
})
.catch(console.warn);
/** Publish a new subscription to the backend. */
async function sendSubscriptionToBackend(api: MastodonClient, subscription: PushSubscription): Promise<WebPushSubscription> {
const params = {
subscription: subscription.toJSON(),
};
const saveSettings = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState().push_notifications;
const alerts = state.alerts;
const data = { alerts };
const me = getState().me;
const response = await api.post('/api/v1/push/subscription', params);
const data = await response.json();
return dispatch(updatePushSubscription({ data })).then(() => {
if (me) {
pushNotificationsSetting.set(me, data);
}
}).catch(console.warn);
};
return webPushSubscriptionSchema.parse(data);
}
export {
register,
saveSettings,
};
/** Check if the VAPID key and endpoint of the subscription match the data in the backend. */
function subscriptionMatchesBackend(subscription: PushSubscription, backend: WebPushSubscription): boolean {
const { applicationServerKey } = subscription.options;
if (subscription.endpoint !== backend.endpoint) {
return false;
}
if (!applicationServerKey) {
return false;
}
const backendKeyBytes: Uint8Array = decodeBase64Url(backend.server_key);
const subscriptionKeyBytes: Uint8Array = new Uint8Array(applicationServerKey);
return backendKeyBytes.toString() === subscriptionKeyBytes.toString();
}

View File

@ -1,39 +0,0 @@
import type { AnyAction } from 'redux';
const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
const setBrowserSupport = (value: boolean) => ({
type: SET_BROWSER_SUPPORT,
value,
});
const setSubscription = (subscription: PushSubscription) => ({
type: SET_SUBSCRIPTION,
subscription,
});
const clearSubscription = () => ({
type: CLEAR_SUBSCRIPTION,
});
const setAlerts = (path: Array<string>, value: any) =>
(dispatch: React.Dispatch<AnyAction>) =>
dispatch({
type: SET_ALERTS,
path,
value,
});
export {
SET_BROWSER_SUPPORT,
SET_SUBSCRIPTION,
CLEAR_SUBSCRIPTION,
SET_ALERTS,
setBrowserSupport,
setSubscription,
clearSubscription,
setAlerts,
};

View File

@ -1,86 +0,0 @@
import api from '../api';
const PUSH_SUBSCRIPTION_CREATE_REQUEST = 'PUSH_SUBSCRIPTION_CREATE_REQUEST';
const PUSH_SUBSCRIPTION_CREATE_SUCCESS = 'PUSH_SUBSCRIPTION_CREATE_SUCCESS';
const PUSH_SUBSCRIPTION_CREATE_FAIL = 'PUSH_SUBSCRIPTION_CREATE_FAIL';
const PUSH_SUBSCRIPTION_FETCH_REQUEST = 'PUSH_SUBSCRIPTION_FETCH_REQUEST';
const PUSH_SUBSCRIPTION_FETCH_SUCCESS = 'PUSH_SUBSCRIPTION_FETCH_SUCCESS';
const PUSH_SUBSCRIPTION_FETCH_FAIL = 'PUSH_SUBSCRIPTION_FETCH_FAIL';
const PUSH_SUBSCRIPTION_UPDATE_REQUEST = 'PUSH_SUBSCRIPTION_UPDATE_REQUEST';
const PUSH_SUBSCRIPTION_UPDATE_SUCCESS = 'PUSH_SUBSCRIPTION_UPDATE_SUCCESS';
const PUSH_SUBSCRIPTION_UPDATE_FAIL = 'PUSH_SUBSCRIPTION_UPDATE_FAIL';
const PUSH_SUBSCRIPTION_DELETE_REQUEST = 'PUSH_SUBSCRIPTION_DELETE_REQUEST';
const PUSH_SUBSCRIPTION_DELETE_SUCCESS = 'PUSH_SUBSCRIPTION_DELETE_SUCCESS';
const PUSH_SUBSCRIPTION_DELETE_FAIL = 'PUSH_SUBSCRIPTION_DELETE_FAIL';
import type { AppDispatch, RootState } from 'soapbox/store';
interface CreatePushSubscriptionParams {
subscription: PushSubscriptionJSON;
data?: {
alerts?: Record<string, boolean>;
policy?: 'all' | 'followed' | 'follower' | 'none';
};
}
const createPushSubscription = (params: CreatePushSubscriptionParams) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: PUSH_SUBSCRIPTION_CREATE_REQUEST, params });
return api(getState).post('/api/v1/push/subscription', params).then(({ data: subscription }) =>
dispatch({ type: PUSH_SUBSCRIPTION_CREATE_SUCCESS, params, subscription }),
).catch(error =>
dispatch({ type: PUSH_SUBSCRIPTION_CREATE_FAIL, params, error }),
);
};
const fetchPushSubscription = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: PUSH_SUBSCRIPTION_FETCH_REQUEST });
return api(getState).get('/api/v1/push/subscription').then(({ data: subscription }) =>
dispatch({ type: PUSH_SUBSCRIPTION_FETCH_SUCCESS, subscription }),
).catch(error =>
dispatch({ type: PUSH_SUBSCRIPTION_FETCH_FAIL, error }),
);
};
const updatePushSubscription = (params: Record<string, any>) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_REQUEST, params });
return api(getState).put('/api/v1/push/subscription', params).then(({ data: subscription }) =>
dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_SUCCESS, params, subscription }),
).catch(error =>
dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_FAIL, params, error }),
);
};
const deletePushSubscription = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: PUSH_SUBSCRIPTION_DELETE_REQUEST });
return api(getState).delete('/api/v1/push/subscription').then(() =>
dispatch({ type: PUSH_SUBSCRIPTION_DELETE_SUCCESS }),
).catch(error =>
dispatch({ type: PUSH_SUBSCRIPTION_DELETE_FAIL, error }),
);
};
export {
PUSH_SUBSCRIPTION_CREATE_REQUEST,
PUSH_SUBSCRIPTION_CREATE_SUCCESS,
PUSH_SUBSCRIPTION_CREATE_FAIL,
PUSH_SUBSCRIPTION_FETCH_REQUEST,
PUSH_SUBSCRIPTION_FETCH_SUCCESS,
PUSH_SUBSCRIPTION_FETCH_FAIL,
PUSH_SUBSCRIPTION_UPDATE_REQUEST,
PUSH_SUBSCRIPTION_UPDATE_SUCCESS,
PUSH_SUBSCRIPTION_UPDATE_FAIL,
PUSH_SUBSCRIPTION_DELETE_REQUEST,
PUSH_SUBSCRIPTION_DELETE_SUCCESS,
PUSH_SUBSCRIPTION_DELETE_FAIL,
createPushSubscription,
fetchPushSubscription,
updatePushSubscription,
deletePushSubscription,
};

View File

@ -10,7 +10,7 @@ import toast from 'soapbox/toast';
const messages = defineMessages({
sucessMessage: { id: 'nostr_signup.captcha_message.sucess', defaultMessage: 'Incredible! You\'ve successfully completed the captcha. Let\'s move on to the next step!' },
sucessMessage: { id: 'nostr_signup.captcha_message.sucess', defaultMessage: 'Incredible! You\'ve successfully completed the captcha.' },
wrongMessage: { id: 'nostr_signup.captcha_message.wrong', defaultMessage: 'Oops! It looks like your captcha response was incorrect. Please try again.' },
errorMessage: { id: 'nostr_signup.captcha_message.error', defaultMessage: 'It seems an error has occurred. Please try again. If the problem persists, please contact us.' },
misbehavingMessage: { id: 'nostr_signup.captcha_message.misbehaving', defaultMessage: 'It looks like we\'re experiencing issues with the {instance}. Please try again. If the error persists, try again later.' },

View File

@ -96,7 +96,7 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
if (!isSubmitDisabled && !createChatMessage.isPending) {
submitMessage();
if (!chat.accepted) {
if (chat.accepted === false) {
acceptChat.mutate();
}
}

View File

@ -1,8 +1,8 @@
import clsx from 'clsx';
import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor, $getRoot } from 'lexical';
import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link, useHistory } from 'react-router-dom';
import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { length } from 'stringz';
import {
@ -39,7 +39,6 @@ import SpoilerInput from './spoiler-input';
import TextCharacterCounter from './text-character-counter';
import UploadForm from './upload-form';
import VisualCharacterCounter from './visual-character-counter';
import Warning from './warning';
import type { Emoji } from 'soapbox/features/emoji';
@ -73,7 +72,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
const compose = useCompose(id);
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
const maxTootChars = instance.configuration.statuses.max_characters;
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
const features = useFeatures();
const {
@ -246,25 +244,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
return (
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
{scheduledStatusCount > 0 && !event && !group && (
<Warning
message={(
<FormattedMessage
id='compose_form.scheduled_statuses.message'
defaultMessage='You have scheduled posts. {click_here} to see them.'
values={{ click_here: (
<Link to='/scheduled_statuses'>
<FormattedMessage
id='compose_form.scheduled_statuses.click_here'
defaultMessage='Click here'
/>
</Link>
) }}
/>)
}
/>
)}
<WarningContainer composeId={id} />
{!shouldCondense && !event && !group && groupId && <ReplyGroupIndicator composeId={id} />}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { useAppSelector, useCompose } from 'soapbox/hooks';
import { useAppSelector, useCompose, useFeatures, useOwnAccount, useSettingsNotifications } from 'soapbox/hooks';
import { selectOwnAccount } from 'soapbox/selectors';
import Warning from '../components/warning';
@ -15,11 +15,61 @@ interface IWarningWrapper {
const WarningWrapper: React.FC<IWarningWrapper> = ({ composeId }) => {
const compose = useCompose(composeId);
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
const { account } = useOwnAccount();
const settingsNotifications = useSettingsNotifications();
const features = useFeatures();
const needsLockWarning = useAppSelector((state) => compose.privacy === 'private' && !selectOwnAccount(state)!.locked);
const hashtagWarning = (compose.privacy !== 'public' && compose.privacy !== 'group') && APPROX_HASHTAG_RE.test(compose.text);
const directMessageWarning = compose.privacy === 'direct';
if (scheduledStatusCount > 0) {
return (
<Warning
message={(
<FormattedMessage
id='compose_form.scheduled_statuses.message'
defaultMessage='You have scheduled posts. {click_here} to see them.'
values={{ click_here: (
<Link to='/scheduled_statuses'>
<FormattedMessage
id='compose_form.scheduled_statuses.click_here'
defaultMessage='Click here'
/>
</Link>
) }}
/>)
}
/>
);
}
if (features.nostr && account?.source?.nostr?.nip05 === undefined) {
return (
<Warning
message={(settingsNotifications.has('needsNip05')) ? (
<FormattedMessage
id='compose_form.nip05.warning'
defaultMessage={'You don\'t have a username configured. {click} to set one up.'}
values={{
click: (
<Link to='/settings/identity'>
<FormattedMessage id='compose_form.nip05.warning.click' defaultMessage='Click here' />
</Link>
),
}}
/>
) : (
<FormattedMessage
id='compose_form.nip05.pending'
defaultMessage='Your username request is under review.'
/>
)}
/>
);
}
if (needsLockWarning) {
return (
<Warning

View File

@ -1,4 +1,4 @@
import data from '@emoji-mart/data/sets/14/twitter.json';
import data from '@emoji-mart/data/sets/15/twitter.json';
export interface NativeEmoji {
unified: string;

View File

@ -5,9 +5,8 @@ import { FormattedMessage } from 'react-intl';
import { fetchAccount } from 'soapbox/actions/accounts';
import { logInNostr } from 'soapbox/actions/nostr';
import { closeSidebar } from 'soapbox/actions/sidebar';
import CopyableInput from 'soapbox/components/copyable-input';
import EmojiGraphic from 'soapbox/components/emoji-graphic';
import { Button, Stack, Modal, FormGroup, Text, Tooltip, HStack } from 'soapbox/components/ui';
import { Button, Stack, Modal, FormGroup, Text, Tooltip, HStack, Input } from 'soapbox/components/ui';
import { useNostr } from 'soapbox/contexts/nostr-context';
import { NKeys } from 'soapbox/features/nostr/keys';
import { useAppDispatch, useInstance } from 'soapbox/hooks';
@ -43,8 +42,6 @@ const KeygenStep: React.FC<IKeygenStep> = ({ onClose }) => {
setDownloaded(true);
};
const handleCopy = () => setDownloaded(true);
const handleNext = async () => {
const signer = NKeys.add(secretKey);
const pubkey = await signer.getPublicKey();
@ -63,10 +60,10 @@ const KeygenStep: React.FC<IKeygenStep> = ({ onClose }) => {
await relay?.event(kind0);
await Promise.all(events.map((event) => relay?.event(event)));
await dispatch(logInNostr(pubkey));
onClose();
await dispatch(logInNostr(pubkey));
if (isMobile) {
dispatch(closeSidebar());
}
@ -79,7 +76,13 @@ const KeygenStep: React.FC<IKeygenStep> = ({ onClose }) => {
<EmojiGraphic emoji='🔑' />
<FormGroup labelText={<FormattedMessage id='nostr_signup.keygen.label_text' defaultMessage='Secret Key' />}>
<CopyableInput value={nsec} type='password' onCopy={handleCopy} />
<Input
type={'password'}
value={nsec}
className='rounded-lg'
outerClassName='grow'
readOnly
/>
</FormGroup>
<Stack className='rounded-xl bg-gray-100 p-4 dark:bg-gray-800'>

View File

@ -8,7 +8,7 @@ import { fetchCustomEmojis } from 'soapbox/actions/custom-emojis';
import { fetchFilters } from 'soapbox/actions/filters';
import { fetchMarker } from 'soapbox/actions/markers';
import { expandNotifications } from 'soapbox/actions/notifications';
import { register as registerPushNotifications } from 'soapbox/actions/push-notifications';
import { registerPushNotifications } from 'soapbox/actions/push-notifications/registerer';
import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses';
import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions';
import { expandHomeTimeline } from 'soapbox/actions/timelines';
@ -16,7 +16,7 @@ import { useUserStream } from 'soapbox/api/hooks';
import SidebarNavigation from 'soapbox/components/sidebar-navigation';
import ThumbNavigation from 'soapbox/components/thumb-navigation';
import { Layout } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useDraggedFiles, useInstance, useLoggedIn } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useDraggedFiles, useInstance, useLoggedIn, useApi } from 'soapbox/hooks';
import AdminPage from 'soapbox/pages/admin-page';
import ChatsPage from 'soapbox/pages/chats-page';
import DefaultPage from 'soapbox/pages/default-page';
@ -34,7 +34,6 @@ import ProfilePage from 'soapbox/pages/profile-page';
import RemoteInstancePage from 'soapbox/pages/remote-instance-page';
import SearchPage from 'soapbox/pages/search-page';
import StatusPage from 'soapbox/pages/status-page';
import { getVapidKey } from 'soapbox/utils/auth';
import BackgroundShapes from './components/background-shapes';
import FloatingActionButton from './components/floating-action-button';
@ -381,6 +380,7 @@ interface IUI {
}
const UI: React.FC<IUI> = ({ children }) => {
const api = useApi();
const history = useHistory();
const dispatch = useAppDispatch();
const node = useRef<HTMLDivElement | null>(null);
@ -388,7 +388,7 @@ const UI: React.FC<IUI> = ({ children }) => {
const { account } = useOwnAccount();
const instance = useInstance();
const features = useFeatures();
const vapidKey = useAppSelector(state => getVapidKey(state));
const vapidKey = instance.instance.configuration.vapid.public_key;
const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.isOpen);
@ -470,7 +470,9 @@ const UI: React.FC<IUI> = ({ children }) => {
}, [!!account]);
useEffect(() => {
dispatch(registerPushNotifications());
if (vapidKey) {
registerPushNotifications(api, vapidKey).catch(console.warn);
}
}, [vapidKey]);
const shouldHideFAB = (): boolean => {

View File

@ -21,4 +21,5 @@ export { useRegistrationStatus } from './useRegistrationStatus';
export { useSettings } from './useSettings';
export { useSoapboxConfig } from './useSoapboxConfig';
export { useSystemTheme } from './useSystemTheme';
export { useTheme } from './useTheme';
export { useTheme } from './useTheme';
export { useSettingsNotifications } from './useSettingsNotifications';

View File

@ -479,6 +479,9 @@
"compose_form.markdown.marked": "Post markdown enabled",
"compose_form.markdown.unmarked": "Post markdown disabled",
"compose_form.message": "Message",
"compose_form.nip05.pending": "Your username request is under review.",
"compose_form.nip05.warning": "You don't have a username configured. {click} to set one up.",
"compose_form.nip05.warning.click": "Click here",
"compose_form.placeholder": "What's on your mind?",
"compose_form.poll.add_option": "Add an answer",
"compose_form.poll.duration": "Poll duration",
@ -1181,7 +1184,7 @@
"nostr_signup.captcha_instruction": "Complete the puzzle by dragging the puzzle piece to the correct position.",
"nostr_signup.captcha_message.error": "It seems an error has occurred. Please try again. If the problem persists, please contact us.",
"nostr_signup.captcha_message.misbehaving": "It looks like we're experiencing issues with the {instance}. Please try again. If the error persists, try again later.",
"nostr_signup.captcha_message.sucess": "Incredible! You've successfully completed the captcha. Let's move on to the next step!",
"nostr_signup.captcha_message.sucess": "Incredible! You've successfully completed the captcha.",
"nostr_signup.captcha_message.wrong": "Oops! It looks like your captcha response was incorrect. Please try again.",
"nostr_signup.captcha_reset_button": "Reset puzzle",
"nostr_signup.captcha_title": "Human Verification",

View File

@ -478,6 +478,9 @@
"compose_form.markdown.marked": "Markdown do post ativado",
"compose_form.markdown.unmarked": "Markdown do post desativado",
"compose_form.message": "Mensagem",
"compose_form.nip05.pending": "Seu pedido de nome de usuário está em revisão.",
"compose_form.nip05.warning": "Você não tem um nome de usuário configurado. {click} para configurá-lo.",
"compose_form.nip05.warning.click": "Clique aqui",
"compose_form.placeholder": "No que você está pensando?",
"compose_form.poll.add_option": "Adicionar uma resposta",
"compose_form.poll.duration": "Duração da enquete",

View File

@ -294,8 +294,7 @@ const useChatActions = (chatId: string) => {
onError: (_error: any, variables, context: any) => {
queryClient.setQueryData(['chats', 'messages', variables.chatId], context.prevChatMessages);
},
onSuccess: async (response, variables, context) => {
const data = await response.json();
onSuccess: (data, variables, context) => {
const nextChat = { ...chat, last_message: data };
updatePageItem(ChatKeys.chatSearch(), nextChat, (o, n) => o.id === n.id);
updatePageItem(

View File

@ -39,7 +39,6 @@ import patron from './patron';
import pending_statuses from './pending-statuses';
import polls from './polls';
import profile_hover_card from './profile-hover-card';
import push_notifications from './push-notifications';
import relationships from './relationships';
import reports from './reports';
import scheduled_statuses from './scheduled-statuses';
@ -97,7 +96,6 @@ const reducers = {
pending_statuses,
polls,
profile_hover_card,
push_notifications,
relationships,
reports,
scheduled_statuses,

View File

@ -1,19 +0,0 @@
import reducer from './push-notifications';
describe('push_notifications reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {} as any).toJS()).toEqual({
subscription: null,
alerts: {
follow: true,
follow_request: true,
favourite: true,
reblog: true,
mention: true,
poll: true,
},
isSubscribed: false,
browserSupport: false,
});
});
});

View File

@ -1,47 +0,0 @@
import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from '../actions/push-notifications';
import type { AnyAction } from 'redux';
const SubscriptionRecord = ImmutableRecord({
id: '',
endpoint: '',
});
const ReducerRecord = ImmutableRecord({
subscription: null as Subscription | null,
alerts: ImmutableMap<string, boolean>({
follow: true,
follow_request: true,
favourite: true,
reblog: true,
mention: true,
poll: true,
}),
isSubscribed: false,
browserSupport: false,
});
type Subscription = ReturnType<typeof SubscriptionRecord>;
export default function push_subscriptions(state = ReducerRecord(), action: AnyAction) {
switch (action.type) {
case SET_SUBSCRIPTION:
return state
.set('subscription', SubscriptionRecord({
id: action.subscription.id,
endpoint: action.subscription.endpoint,
}))
.set('alerts', ImmutableMap(action.subscription.alerts))
.set('isSubscribed', true);
case SET_BROWSER_SUPPORT:
return state.set('browserSupport', action.value);
case CLEAR_SUBSCRIPTION:
return ReducerRecord();
case SET_ALERTS:
return state.setIn(action.path, action.value);
default:
return state;
}
}

View File

@ -1,10 +1,12 @@
import { z } from 'zod';
import { coerceObject } from './utils';
/** https://docs.joinmastodon.org/entities/WebPushSubscription/ */
const webPushSubscriptionSchema = z.object({
id: z.coerce.string(),
endpoint: z.string().url(),
alerts: z.object({
alerts: coerceObject({
mention: z.boolean().optional(),
status: z.boolean().optional(),
reblog: z.boolean().optional(),
@ -15,7 +17,7 @@ const webPushSubscriptionSchema = z.object({
update: z.boolean().optional(),
'admin.sign_up': z.boolean().optional(),
'admin.report': z.boolean().optional(),
}).optional(),
}),
server_key: z.string(),
});

View File

@ -62,8 +62,4 @@ export const getAuthUserUrl = (state: RootState) => {
].filter(url => url)).find(isURL);
};
/** Get the VAPID public key. */
export const getVapidKey = (state: RootState) =>
state.auth.app.vapid_key || state.instance.pleroma.vapid_public_key;
export const getMeUrl = (state: RootState) => selectOwnAccount(state)?.url;

View File

@ -1,10 +1,8 @@
import * as base64 from './base64';
import { decodeBase64 } from './base64';
describe('base64', () => {
describe('decode', () => {
it('returns a uint8 array', () => {
const arr = base64.decode('dGVzdA==');
expect(arr).toEqual(new Uint8Array([116, 101, 115, 116]));
});
describe('decodeBase64', () => {
it('returns a uint8 array', () => {
const arr = decodeBase64('dGVzdA==');
expect(arr).toEqual(new Uint8Array([116, 101, 115, 116]));
});
});

View File

@ -1,4 +1,4 @@
export const decode = (base64: string) => {
export function decodeBase64(base64: string): Uint8Array {
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
@ -7,4 +7,14 @@ export const decode = (base64: string) => {
}
return outputArray;
};
}
/** Taken from https://www.npmjs.com/package/web-push */
export function decodeBase64Url(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
return decodeBase64(base64);
}

View File

@ -76,7 +76,7 @@ export default defineConfig(({ command }) => ({
}),
viteStaticCopy({
targets: [{
src: './node_modules/twemoji/assets/svg/*',
src: './node_modules/@twemoji/svg/*',
dest: 'packs/emoji/',
}, {
src: './src/instance',

View File

@ -1009,10 +1009,10 @@
resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b"
integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==
"@emoji-mart/data@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.1.2.tgz#777c976f8f143df47cbb23a7077c9ca9fe5fc513"
integrity sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg==
"@emoji-mart/data@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.2.1.tgz#0ad70c662e3bc603e23e7d98413bd1e64c4fcb6c"
integrity sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==
"@es-joy/jsdoccomment@~0.41.0":
version "0.41.0"
@ -2465,6 +2465,11 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@twemoji/svg@^15.0.0":
version "15.0.0"
resolved "https://registry.yarnpkg.com/@twemoji/svg/-/svg-15.0.0.tgz#0e3828c654726f1848fe11f31ef4e8a75854cc7f"
integrity sha512-ZSPef2B6nBaYnfgdTbAy4jgW95o7pi2xPGwGCU+WMTxo7J6B1lMPTWwSq/wTuiMq+N0khQ90CcvYp1wFoQpo/w==
"@types/aria-query@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc"
@ -4322,15 +4327,15 @@ electron-to-chromium@^1.5.28:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz#aee074e202c6ee8a0030a9c2ef0b3fe9f967d576"
integrity sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw==
emoji-datasource@14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/emoji-datasource/-/emoji-datasource-14.0.0.tgz#99529a62f3a86546fc670c09b672ddc9f24f3d44"
integrity sha512-SoOv0lSa+9/2X9ulSRDhu2u1zAOaOv5vtMY3OYUDcQCoReEh0/3eQAMuBM9LyD7Hy3G4K7mDPDqVeHUWvy7cow==
emoji-datasource@15.0.1:
version "15.0.1"
resolved "https://registry.yarnpkg.com/emoji-datasource/-/emoji-datasource-15.0.1.tgz#6cc7676e4d48d7559c2e068ffcacf84ec653584c"
integrity sha512-aF5Q6LCKXzJzpG4K0ETiItuzz0xLYxNexR9qWw45/shuuEDWZkOIbeGHA23uopOSYA/LmeZIXIFsySCx+YKg2g==
emoji-mart@^5.5.2:
version "5.5.2"
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af"
integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==
emoji-mart@^5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023"
integrity sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==
emoji-regex@10.3.0, emoji-regex@^10.2.1:
version "10.3.0"
@ -5027,15 +5032,6 @@ fs-extra@^11.1.0:
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-extra@^8.0.1:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^9.0.1:
version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
@ -5978,22 +5974,6 @@ json5@^2.1.2, json5@^2.2.0, json5@^2.2.3:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922"
integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==
dependencies:
universalify "^0.1.2"
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@ -8608,20 +8588,6 @@ tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
twemoji-parser@14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-14.0.0.tgz#13dabcb6d3a261d9efbf58a1666b182033bf2b62"
integrity sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==
"twemoji@https://github.com/twitter/twemoji#v14.0.2":
version "14.0.2"
resolved "https://github.com/twitter/twemoji#7a3dad4a4da30497093dab22eafba135f02308e1"
dependencies:
fs-extra "^8.0.1"
jsonfile "^5.0.0"
twemoji-parser "14.0.0"
universalify "^0.1.2"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@ -8748,11 +8714,6 @@ unique-string@^2.0.0:
dependencies:
crypto-random-string "^2.0.0"
universalify@^0.1.0, universalify@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
universalify@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"