diff --git a/package.json b/package.json index 825cb97b9..a1da1e162 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/actions/push-notifications/index.ts b/src/actions/push-notifications/index.ts deleted file mode 100644 index 69fdf2787..000000000 --- a/src/actions/push-notifications/index.ts +++ /dev/null @@ -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, value: any) => - (dispatch: AppDispatch) => { - dispatch(setAlerts(path, value)); - dispatch(saveSettings() as any); - }; diff --git a/src/actions/push-notifications/registerer.ts b/src/actions/push-notifications/registerer.ts index 9937fd938..f3e4d4a6f 100644 --- a/src/actions/push-notifications/registerer.ts +++ b/src/actions/push-notifications/registerer.ts @@ -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(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 { + 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 { + 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 }) => { - // 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 { + 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(); +} \ No newline at end of file diff --git a/src/actions/push-notifications/setter.ts b/src/actions/push-notifications/setter.ts deleted file mode 100644 index 739427e7e..000000000 --- a/src/actions/push-notifications/setter.ts +++ /dev/null @@ -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, value: any) => - (dispatch: React.Dispatch) => - dispatch({ - type: SET_ALERTS, - path, - value, - }); - -export { - SET_BROWSER_SUPPORT, - SET_SUBSCRIPTION, - CLEAR_SUBSCRIPTION, - SET_ALERTS, - setBrowserSupport, - setSubscription, - clearSubscription, - setAlerts, -}; diff --git a/src/actions/push-subscriptions.ts b/src/actions/push-subscriptions.ts deleted file mode 100644 index 19b047950..000000000 --- a/src/actions/push-subscriptions.ts +++ /dev/null @@ -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; - 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) => - (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, -}; diff --git a/src/api/hooks/captcha/useCaptcha.ts b/src/api/hooks/captcha/useCaptcha.ts index a79da8fa9..01b9c9926 100644 --- a/src/api/hooks/captcha/useCaptcha.ts +++ b/src/api/hooks/captcha/useCaptcha.ts @@ -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.' }, diff --git a/src/features/chats/components/chat.tsx b/src/features/chats/components/chat.tsx index 627fe2972..e7ef9c68d 100644 --- a/src/features/chats/components/chat.tsx +++ b/src/features/chats/components/chat.tsx @@ -96,7 +96,7 @@ const Chat: React.FC = ({ chat, inputRef, className }) => { if (!isSubmitDisabled && !createChatMessage.isPending) { submitMessage(); - if (!chat.accepted) { + if (chat.accepted === false) { acceptChat.mutate(); } } diff --git a/src/features/compose/components/compose-form.tsx b/src/features/compose/components/compose-form.tsx index d4d61e834..8b0f7b19a 100644 --- a/src/features/compose/components/compose-form.tsx +++ b/src/features/compose/components/compose-form.tsx @@ -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, 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, shouldCondense, autoFocus, clickab return ( - {scheduledStatusCount > 0 && !event && !group && ( - - - - ) }} - />) - } - /> - )} - {!shouldCondense && !event && !group && groupId && } diff --git a/src/features/compose/containers/warning-container.tsx b/src/features/compose/containers/warning-container.tsx index 858404c27..8ebfd5f8f 100644 --- a/src/features/compose/containers/warning-container.tsx +++ b/src/features/compose/containers/warning-container.tsx @@ -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 = ({ 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 ( + + + + ) }} + />) + } + /> + ); + } + + if (features.nostr && account?.source?.nostr?.nip05 === undefined) { + return ( + + + + ), + }} + /> + ) : ( + + )} + /> + ); + } + if (needsLockWarning) { return ( = ({ 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 = ({ 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 = ({ onClose }) => { }> - + diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index 711f14289..38a06855b 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -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 = ({ children }) => { + const api = useApi(); const history = useHistory(); const dispatch = useAppDispatch(); const node = useRef(null); @@ -388,7 +388,7 @@ const UI: React.FC = ({ 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 = ({ children }) => { }, [!!account]); useEffect(() => { - dispatch(registerPushNotifications()); + if (vapidKey) { + registerPushNotifications(api, vapidKey).catch(console.warn); + } }, [vapidKey]); const shouldHideFAB = (): boolean => { diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 8d3b7a510..abfbd726c 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -21,4 +21,5 @@ export { useRegistrationStatus } from './useRegistrationStatus'; export { useSettings } from './useSettings'; export { useSoapboxConfig } from './useSoapboxConfig'; export { useSystemTheme } from './useSystemTheme'; -export { useTheme } from './useTheme'; \ No newline at end of file +export { useTheme } from './useTheme'; +export { useSettingsNotifications } from './useSettingsNotifications'; \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index deeb4c619..8e8cef81a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index 8a0a94928..3176ebb46 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -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", diff --git a/src/queries/chats.ts b/src/queries/chats.ts index 9eb1f10ee..2d6cb4118 100644 --- a/src/queries/chats.ts +++ b/src/queries/chats.ts @@ -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( diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 4534812dd..925e40d8a 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -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, diff --git a/src/reducers/push-notifications.test.ts b/src/reducers/push-notifications.test.ts deleted file mode 100644 index 021cb1771..000000000 --- a/src/reducers/push-notifications.test.ts +++ /dev/null @@ -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, - }); - }); -}); diff --git a/src/reducers/push-notifications.ts b/src/reducers/push-notifications.ts deleted file mode 100644 index a2b8ca51f..000000000 --- a/src/reducers/push-notifications.ts +++ /dev/null @@ -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({ - follow: true, - follow_request: true, - favourite: true, - reblog: true, - mention: true, - poll: true, - }), - isSubscribed: false, - browserSupport: false, -}); - -type Subscription = ReturnType; - -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; - } -} diff --git a/src/schemas/web-push.ts b/src/schemas/web-push.ts index 9162f2bfb..f0553dc32 100644 --- a/src/schemas/web-push.ts +++ b/src/schemas/web-push.ts @@ -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(), }); diff --git a/src/utils/auth.ts b/src/utils/auth.ts index b5d9fb120..73757fb80 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -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; \ No newline at end of file diff --git a/src/utils/base64.test.ts b/src/utils/base64.test.ts index ae0782677..d2537d99b 100644 --- a/src/utils/base64.test.ts +++ b/src/utils/base64.test.ts @@ -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])); }); }); diff --git a/src/utils/base64.ts b/src/utils/base64.ts index c512a6594..ebdf57ae8 100644 --- a/src/utils/base64.ts +++ b/src/utils/base64.ts @@ -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); +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 9e3d58410..21836551e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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', diff --git a/yarn.lock b/yarn.lock index 02e4db89a..7850fd2ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"