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": { "dependencies": {
"@akryum/flexsearch-es": "^0.7.32", "@akryum/flexsearch-es": "^0.7.32",
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.2.1",
"@floating-ui/react": "^0.26.0", "@floating-ui/react": "^0.26.0",
"@fontsource/inter": "^5.0.0", "@fontsource/inter": "^5.0.0",
"@fontsource/noto-sans-javanese": "^5.0.16", "@fontsource/noto-sans-javanese": "^5.0.16",
@ -72,6 +72,7 @@
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@tanstack/react-query": "^5.59.13", "@tanstack/react-query": "^5.59.13",
"@twemoji/svg": "^15.0.0",
"@types/escape-html": "^1.0.1", "@types/escape-html": "^1.0.1",
"@types/http-link-header": "^1.0.3", "@types/http-link-header": "^1.0.3",
"@types/leaflet": "^1.8.0", "@types/leaflet": "^1.8.0",
@ -102,8 +103,8 @@
"cryptocurrency-icons": "^0.18.1", "cryptocurrency-icons": "^0.18.1",
"cssnano": "^6.0.0", "cssnano": "^6.0.0",
"detect-passive-events": "^2.0.0", "detect-passive-events": "^2.0.0",
"emoji-datasource": "14.0.0", "emoji-datasource": "15.0.1",
"emoji-mart": "^5.5.2", "emoji-mart": "^5.6.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"eslint-plugin-formatjs": "^4.12.2", "eslint-plugin-formatjs": "^4.12.2",
"exifr": "^7.1.3", "exifr": "^7.1.3",
@ -152,7 +153,6 @@
"sass": "^1.79.5", "sass": "^1.79.5",
"semver": "^7.3.8", "semver": "^7.3.8",
"stringz": "^2.0.0", "stringz": "^2.0.0",
"twemoji": "https://github.com/twitter/twemoji#v14.0.2",
"type-fest": "^4.0.0", "type-fest": "^4.0.0",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"vite": "^5.4.8", "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'; /* eslint-disable compat/compat */
import { pushNotificationsSetting } from 'soapbox/settings'; import { HTTPError } from 'soapbox/api/HTTPError';
import { getVapidKey } from 'soapbox/utils/auth'; import { MastodonClient } from 'soapbox/api/MastodonClient';
import { decode as decodeBase64 } from 'soapbox/utils/base64'; import { WebPushSubscription, webPushSubscriptionSchema } from 'soapbox/schemas/web-push';
import { decodeBase64Url } 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));
};
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload // 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 supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
const register = () => /**
(dispatch: AppDispatch, getState: () => RootState) => { * Register web push notifications.
const me = getState().me; * This function creates a subscription if one hasn't been created already, and syncronizes it with the backend.
const vapidKey = getVapidKey(getState()); */
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) { if (created) {
console.warn('Your browser does not support Web Push Notifications.'); await sendSubscriptionToBackend(api, subscription);
return; 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) { /** Publish a new subscription to the backend. */
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); async function sendSubscriptionToBackend(api: MastodonClient, subscription: PushSubscription): Promise<WebPushSubscription> {
return; const params = {
} subscription: subscription.toJSON(),
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);
}; };
const saveSettings = () => const response = await api.post('/api/v1/push/subscription', params);
(dispatch: AppDispatch, getState: () => RootState) => { const data = await response.json();
const state = getState().push_notifications;
const alerts = state.alerts;
const data = { alerts };
const me = getState().me;
return dispatch(updatePushSubscription({ data })).then(() => { return webPushSubscriptionSchema.parse(data);
if (me) { }
pushNotificationsSetting.set(me, data);
}
}).catch(console.warn);
};
export { /** Check if the VAPID key and endpoint of the subscription match the data in the backend. */
register, function subscriptionMatchesBackend(subscription: PushSubscription, backend: WebPushSubscription): boolean {
saveSettings, 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({ 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.' }, 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.' }, 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.' }, 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) { if (!isSubmitDisabled && !createChatMessage.isPending) {
submitMessage(); submitMessage();
if (!chat.accepted) { if (chat.accepted === false) {
acceptChat.mutate(); acceptChat.mutate();
} }
} }

View File

@ -1,8 +1,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor, $getRoot } from 'lexical'; import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor, $getRoot } from 'lexical';
import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react'; import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { Link, useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { length } from 'stringz'; import { length } from 'stringz';
import { import {
@ -39,7 +39,6 @@ import SpoilerInput from './spoiler-input';
import TextCharacterCounter from './text-character-counter'; import TextCharacterCounter from './text-character-counter';
import UploadForm from './upload-form'; import UploadForm from './upload-form';
import VisualCharacterCounter from './visual-character-counter'; import VisualCharacterCounter from './visual-character-counter';
import Warning from './warning';
import type { Emoji } from 'soapbox/features/emoji'; 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 compose = useCompose(id);
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden); const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
const maxTootChars = instance.configuration.statuses.max_characters; const maxTootChars = instance.configuration.statuses.max_characters;
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
const features = useFeatures(); const features = useFeatures();
const { const {
@ -246,25 +244,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
return ( return (
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}> <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} /> <WarningContainer composeId={id} />
{!shouldCondense && !event && !group && groupId && <ReplyGroupIndicator 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 { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; 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 { selectOwnAccount } from 'soapbox/selectors';
import Warning from '../components/warning'; import Warning from '../components/warning';
@ -15,11 +15,61 @@ interface IWarningWrapper {
const WarningWrapper: React.FC<IWarningWrapper> = ({ composeId }) => { const WarningWrapper: React.FC<IWarningWrapper> = ({ composeId }) => {
const compose = useCompose(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 needsLockWarning = useAppSelector((state) => compose.privacy === 'private' && !selectOwnAccount(state)!.locked);
const hashtagWarning = (compose.privacy !== 'public' && compose.privacy !== 'group') && APPROX_HASHTAG_RE.test(compose.text); const hashtagWarning = (compose.privacy !== 'public' && compose.privacy !== 'group') && APPROX_HASHTAG_RE.test(compose.text);
const directMessageWarning = compose.privacy === 'direct'; 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) { if (needsLockWarning) {
return ( return (
<Warning <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 { export interface NativeEmoji {
unified: string; unified: string;

View File

@ -5,9 +5,8 @@ import { FormattedMessage } from 'react-intl';
import { fetchAccount } from 'soapbox/actions/accounts'; import { fetchAccount } from 'soapbox/actions/accounts';
import { logInNostr } from 'soapbox/actions/nostr'; import { logInNostr } from 'soapbox/actions/nostr';
import { closeSidebar } from 'soapbox/actions/sidebar'; import { closeSidebar } from 'soapbox/actions/sidebar';
import CopyableInput from 'soapbox/components/copyable-input';
import EmojiGraphic from 'soapbox/components/emoji-graphic'; 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 { useNostr } from 'soapbox/contexts/nostr-context';
import { NKeys } from 'soapbox/features/nostr/keys'; import { NKeys } from 'soapbox/features/nostr/keys';
import { useAppDispatch, useInstance } from 'soapbox/hooks'; import { useAppDispatch, useInstance } from 'soapbox/hooks';
@ -43,8 +42,6 @@ const KeygenStep: React.FC<IKeygenStep> = ({ onClose }) => {
setDownloaded(true); setDownloaded(true);
}; };
const handleCopy = () => setDownloaded(true);
const handleNext = async () => { const handleNext = async () => {
const signer = NKeys.add(secretKey); const signer = NKeys.add(secretKey);
const pubkey = await signer.getPublicKey(); const pubkey = await signer.getPublicKey();
@ -63,10 +60,10 @@ const KeygenStep: React.FC<IKeygenStep> = ({ onClose }) => {
await relay?.event(kind0); await relay?.event(kind0);
await Promise.all(events.map((event) => relay?.event(event))); await Promise.all(events.map((event) => relay?.event(event)));
await dispatch(logInNostr(pubkey));
onClose(); onClose();
await dispatch(logInNostr(pubkey));
if (isMobile) { if (isMobile) {
dispatch(closeSidebar()); dispatch(closeSidebar());
} }
@ -79,7 +76,13 @@ const KeygenStep: React.FC<IKeygenStep> = ({ onClose }) => {
<EmojiGraphic emoji='🔑' /> <EmojiGraphic emoji='🔑' />
<FormGroup labelText={<FormattedMessage id='nostr_signup.keygen.label_text' defaultMessage='Secret Key' />}> <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> </FormGroup>
<Stack className='rounded-xl bg-gray-100 p-4 dark:bg-gray-800'> <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 { fetchFilters } from 'soapbox/actions/filters';
import { fetchMarker } from 'soapbox/actions/markers'; import { fetchMarker } from 'soapbox/actions/markers';
import { expandNotifications } from 'soapbox/actions/notifications'; 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 { 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';
@ -16,7 +16,7 @@ import { useUserStream } from 'soapbox/api/hooks';
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';
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 AdminPage from 'soapbox/pages/admin-page';
import ChatsPage from 'soapbox/pages/chats-page'; import ChatsPage from 'soapbox/pages/chats-page';
import DefaultPage from 'soapbox/pages/default-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 RemoteInstancePage from 'soapbox/pages/remote-instance-page';
import SearchPage from 'soapbox/pages/search-page'; import SearchPage from 'soapbox/pages/search-page';
import StatusPage from 'soapbox/pages/status-page'; import StatusPage from 'soapbox/pages/status-page';
import { getVapidKey } from 'soapbox/utils/auth';
import BackgroundShapes from './components/background-shapes'; import BackgroundShapes from './components/background-shapes';
import FloatingActionButton from './components/floating-action-button'; import FloatingActionButton from './components/floating-action-button';
@ -381,6 +380,7 @@ interface IUI {
} }
const UI: React.FC<IUI> = ({ children }) => { const UI: React.FC<IUI> = ({ children }) => {
const api = useApi();
const history = useHistory(); const history = useHistory();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const node = useRef<HTMLDivElement | null>(null); const node = useRef<HTMLDivElement | null>(null);
@ -388,7 +388,7 @@ const UI: React.FC<IUI> = ({ children }) => {
const { account } = useOwnAccount(); const { account } = useOwnAccount();
const instance = useInstance(); const instance = useInstance();
const features = useFeatures(); 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); const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.isOpen);
@ -470,7 +470,9 @@ const UI: React.FC<IUI> = ({ children }) => {
}, [!!account]); }, [!!account]);
useEffect(() => { useEffect(() => {
dispatch(registerPushNotifications()); if (vapidKey) {
registerPushNotifications(api, vapidKey).catch(console.warn);
}
}, [vapidKey]); }, [vapidKey]);
const shouldHideFAB = (): boolean => { const shouldHideFAB = (): boolean => {

View File

@ -22,3 +22,4 @@ export { useSettings } from './useSettings';
export { useSoapboxConfig } from './useSoapboxConfig'; export { useSoapboxConfig } from './useSoapboxConfig';
export { useSystemTheme } from './useSystemTheme'; 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.marked": "Post markdown enabled",
"compose_form.markdown.unmarked": "Post markdown disabled", "compose_form.markdown.unmarked": "Post markdown disabled",
"compose_form.message": "Message", "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.placeholder": "What's on your mind?",
"compose_form.poll.add_option": "Add an answer", "compose_form.poll.add_option": "Add an answer",
"compose_form.poll.duration": "Poll duration", "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_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.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.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_message.wrong": "Oops! It looks like your captcha response was incorrect. Please try again.",
"nostr_signup.captcha_reset_button": "Reset puzzle", "nostr_signup.captcha_reset_button": "Reset puzzle",
"nostr_signup.captcha_title": "Human Verification", "nostr_signup.captcha_title": "Human Verification",

View File

@ -478,6 +478,9 @@
"compose_form.markdown.marked": "Markdown do post ativado", "compose_form.markdown.marked": "Markdown do post ativado",
"compose_form.markdown.unmarked": "Markdown do post desativado", "compose_form.markdown.unmarked": "Markdown do post desativado",
"compose_form.message": "Mensagem", "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.placeholder": "No que você está pensando?",
"compose_form.poll.add_option": "Adicionar uma resposta", "compose_form.poll.add_option": "Adicionar uma resposta",
"compose_form.poll.duration": "Duração da enquete", "compose_form.poll.duration": "Duração da enquete",

View File

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

View File

@ -39,7 +39,6 @@ import patron from './patron';
import pending_statuses from './pending-statuses'; import pending_statuses from './pending-statuses';
import polls from './polls'; import polls from './polls';
import profile_hover_card from './profile-hover-card'; import profile_hover_card from './profile-hover-card';
import push_notifications from './push-notifications';
import relationships from './relationships'; import relationships from './relationships';
import reports from './reports'; import reports from './reports';
import scheduled_statuses from './scheduled-statuses'; import scheduled_statuses from './scheduled-statuses';
@ -97,7 +96,6 @@ const reducers = {
pending_statuses, pending_statuses,
polls, polls,
profile_hover_card, profile_hover_card,
push_notifications,
relationships, relationships,
reports, reports,
scheduled_statuses, 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 { z } from 'zod';
import { coerceObject } from './utils';
/** https://docs.joinmastodon.org/entities/WebPushSubscription/ */ /** https://docs.joinmastodon.org/entities/WebPushSubscription/ */
const webPushSubscriptionSchema = z.object({ const webPushSubscriptionSchema = z.object({
id: z.coerce.string(), id: z.coerce.string(),
endpoint: z.string().url(), endpoint: z.string().url(),
alerts: z.object({ alerts: coerceObject({
mention: z.boolean().optional(), mention: z.boolean().optional(),
status: z.boolean().optional(), status: z.boolean().optional(),
reblog: z.boolean().optional(), reblog: z.boolean().optional(),
@ -15,7 +17,7 @@ const webPushSubscriptionSchema = z.object({
update: z.boolean().optional(), update: z.boolean().optional(),
'admin.sign_up': z.boolean().optional(), 'admin.sign_up': z.boolean().optional(),
'admin.report': z.boolean().optional(), 'admin.report': z.boolean().optional(),
}).optional(), }),
server_key: z.string(), server_key: z.string(),
}); });

View File

@ -62,8 +62,4 @@ export const getAuthUserUrl = (state: RootState) => {
].filter(url => url)).find(isURL); ].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; 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('decodeBase64', () => {
describe('decode', () => { it('returns a uint8 array', () => {
it('returns a uint8 array', () => { const arr = decodeBase64('dGVzdA==');
const arr = base64.decode('dGVzdA=='); expect(arr).toEqual(new Uint8Array([116, 101, 115, 116]));
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 rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length); const outputArray = new Uint8Array(rawData.length);
@ -7,4 +7,14 @@ export const decode = (base64: string) => {
} }
return outputArray; 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({ viteStaticCopy({
targets: [{ targets: [{
src: './node_modules/twemoji/assets/svg/*', src: './node_modules/@twemoji/svg/*',
dest: 'packs/emoji/', dest: 'packs/emoji/',
}, { }, {
src: './src/instance', 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" resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b"
integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg== integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==
"@emoji-mart/data@^1.1.2": "@emoji-mart/data@^1.2.1":
version "1.1.2" version "1.2.1"
resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.1.2.tgz#777c976f8f143df47cbb23a7077c9ca9fe5fc513" resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.2.1.tgz#0ad70c662e3bc603e23e7d98413bd1e64c4fcb6c"
integrity sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg== integrity sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==
"@es-joy/jsdoccomment@~0.41.0": "@es-joy/jsdoccomment@~0.41.0":
version "0.41.0" version "0.41.0"
@ -2465,6 +2465,11 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== 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": "@types/aria-query@^5.0.1":
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" 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" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz#aee074e202c6ee8a0030a9c2ef0b3fe9f967d576"
integrity sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw== integrity sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw==
emoji-datasource@14.0.0: emoji-datasource@15.0.1:
version "14.0.0" version "15.0.1"
resolved "https://registry.yarnpkg.com/emoji-datasource/-/emoji-datasource-14.0.0.tgz#99529a62f3a86546fc670c09b672ddc9f24f3d44" resolved "https://registry.yarnpkg.com/emoji-datasource/-/emoji-datasource-15.0.1.tgz#6cc7676e4d48d7559c2e068ffcacf84ec653584c"
integrity sha512-SoOv0lSa+9/2X9ulSRDhu2u1zAOaOv5vtMY3OYUDcQCoReEh0/3eQAMuBM9LyD7Hy3G4K7mDPDqVeHUWvy7cow== integrity sha512-aF5Q6LCKXzJzpG4K0ETiItuzz0xLYxNexR9qWw45/shuuEDWZkOIbeGHA23uopOSYA/LmeZIXIFsySCx+YKg2g==
emoji-mart@^5.5.2: emoji-mart@^5.6.0:
version "5.5.2" version "5.6.0"
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af" resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023"
integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A== integrity sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==
emoji-regex@10.3.0, emoji-regex@^10.2.1: emoji-regex@10.3.0, emoji-regex@^10.2.1:
version "10.3.0" version "10.3.0"
@ -5027,15 +5032,6 @@ fs-extra@^11.1.0:
jsonfile "^6.0.1" jsonfile "^6.0.1"
universalify "^2.0.0" 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: fs-extra@^9.0.1:
version "9.1.0" version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" 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" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== 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: jsonfile@^6.0.1:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" 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" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 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: type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@ -8748,11 +8714,6 @@ unique-string@^2.0.0:
dependencies: dependencies:
crypto-random-string "^2.0.0" 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: universalify@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"