Merge branch 'webpush-rewrite' into 'main'
Rewrite Web Push registerer See merge request soapbox-pub/soapbox!3161
This commit is contained in:
commit
daff662210
|
@ -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);
|
||||
};
|
|
@ -1,157 +1,108 @@
|
|||
import { createPushSubscription, updatePushSubscription } from 'soapbox/actions/push-subscriptions';
|
||||
import { pushNotificationsSetting } from 'soapbox/settings';
|
||||
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 = (vapidKey: string, registration: ServiceWorkerRegistration) =>
|
||||
registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidKey),
|
||||
});
|
||||
|
||||
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 = (vapidKey: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const me = getState().me;
|
||||
/**
|
||||
* 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(vapidKey, registration);
|
||||
}).then(
|
||||
(subscription: PushSubscription) => dispatch(sendSubscriptionToBackend(subscription, me) as any));
|
||||
}
|
||||
}
|
||||
|
||||
// No subscription, try to subscribe
|
||||
return subscribe(vapidKey, registration)
|
||||
.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();
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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';
|
||||
|
@ -380,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);
|
||||
|
@ -470,7 +471,7 @@ const UI: React.FC<IUI> = ({ children }) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (vapidKey) {
|
||||
dispatch(registerPushNotifications(vapidKey));
|
||||
registerPushNotifications(api, vapidKey).catch(console.warn);
|
||||
}
|
||||
}, [vapidKey]);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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]));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue