fix: manage webpush notifications (#4059)

pull/3839/head
Brandon Cohen 1 month ago committed by GitHub
parent f0af3ca61d
commit efc3821d26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3579,6 +3579,8 @@ paths:
type: string
p256dh:
type: string
userAgent:
type: string
required:
- endpoint
- auth
@ -3586,6 +3588,88 @@ paths:
responses:
'204':
description: Successfully registered push subscription
/user/{userId}/pushSubscriptions:
get:
summary: Get all web push notification settings for a user
description: |
Returns all web push notification settings for a user in a JSON object.
tags:
- users
parameters:
- in: path
name: userId
required: true
schema:
type: number
responses:
'200':
description: User web push notification settings in JSON
content:
application/json:
schema:
type: object
properties:
endpoint:
type: string
p256dh:
type: string
auth:
type: string
userAgent:
type: string
/user/{userId}/pushSubscription/{key}:
get:
summary: Get web push notification settings for a user
description: |
Returns web push notification settings for a user in a JSON object.
tags:
- users
parameters:
- in: path
name: userId
required: true
schema:
type: number
- in: path
name: key
required: true
schema:
type: string
responses:
'200':
description: User web push notification settings in JSON
content:
application/json:
schema:
type: object
properties:
endpoint:
type: string
p256dh:
type: string
auth:
type: string
userAgent:
type: string
delete:
summary: Delete user push subscription by key
description: Deletes the user push subscription with the provided key.
tags:
- users
parameters:
- in: path
name: userId
required: true
schema:
type: number
- in: path
name: key
required: true
schema:
type: string
responses:
'204':
description: Successfully removed user push subscription
/user/{userId}:
get:
summary: Get user by ID

@ -38,6 +38,7 @@
"@supercharge/request-ip": "1.2.0",
"@svgr/webpack": "6.5.1",
"@tanem/react-nprogress": "5.0.30",
"@types/ua-parser-js": "^0.7.36",
"ace-builds": "1.15.2",
"axios": "1.3.4",
"axios-rate-limit": "1.3.0",
@ -90,6 +91,7 @@
"swagger-ui-express": "4.6.2",
"swr": "2.0.4",
"typeorm": "0.3.12",
"ua-parser-js": "^1.0.35",
"web-push": "3.5.0",
"winston": "3.8.2",
"winston-daily-rotate-file": "4.7.1",

@ -1,4 +1,10 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from './User';
@Entity()
@ -18,9 +24,15 @@ export class UserPushSubscription {
@Column()
public p256dh: string;
@Column({ unique: true })
@Column()
public auth: string;
@Column({ nullable: true })
public userAgent: string;
@CreateDateColumn({ nullable: true })
public createdAt: Date;
constructor(init?: Partial<UserPushSubscription>) {
Object.assign(this, init);
}

@ -0,0 +1,51 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateWebPush1740717744278 implements MigrationInterface {
name = 'UpdateWebPush1740717744278';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
}
}

@ -145,13 +145,15 @@ router.post<
endpoint: string;
p256dh: string;
auth: string;
userAgent: string;
}
>('/registerPushSubscription', async (req, res, next) => {
try {
const userPushSubRepository = getRepository(UserPushSubscription);
const existingSubs = await userPushSubRepository.find({
where: { auth: req.body.auth },
relations: { user: true },
where: { auth: req.body.auth, user: { id: req.user?.id } },
});
if (existingSubs.length > 0) {
@ -166,6 +168,7 @@ router.post<
auth: req.body.auth,
endpoint: req.body.endpoint,
p256dh: req.body.p256dh,
userAgent: req.body.userAgent,
user: req.user,
});
@ -180,6 +183,79 @@ router.post<
}
});
router.get<{ userId: number }>(
'/:userId/pushSubscriptions',
async (req, res, next) => {
try {
const userPushSubRepository = getRepository(UserPushSubscription);
const userPushSubs = await userPushSubRepository.find({
relations: { user: true },
where: { user: { id: req.params.userId } },
});
return res.status(200).json(userPushSubs);
} catch (e) {
next({ status: 404, message: 'User subscriptions not found.' });
}
}
);
router.get<{ userId: number; key: string }>(
'/:userId/pushSubscription/:key',
async (req, res, next) => {
try {
const userPushSubRepository = getRepository(UserPushSubscription);
const userPushSub = await userPushSubRepository.findOneOrFail({
relations: {
user: true,
},
where: {
user: { id: req.params.userId },
p256dh: req.params.key,
},
});
return res.status(200).json(userPushSub);
} catch (e) {
next({ status: 404, message: 'User subscription not found.' });
}
}
);
router.delete<{ userId: number; key: string }>(
'/:userId/pushSubscription/:key',
async (req, res, next) => {
try {
const userPushSubRepository = getRepository(UserPushSubscription);
const userPushSub = await userPushSubRepository.findOneOrFail({
relations: {
user: true,
},
where: {
user: { id: req.params.userId },
p256dh: req.params.key,
},
});
await userPushSubRepository.remove(userPushSub);
return res.status(204).send();
} catch (e) {
logger.error('Something went wrong deleting the user push subcription', {
label: 'API',
key: req.params.key,
errorMessage: e.message,
});
return next({
status: 500,
message: 'User push subcription not found',
});
}
}
);
router.get<{ id: string }>('/:id', async (req, res, next) => {
try {
const userRepository = getRepository(User);

@ -1,11 +1,9 @@
/* eslint-disable no-console */
import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser';
import axios from 'axios';
import { useEffect } from 'react';
const ServiceWorkerSetup = () => {
const { currentSettings } = useSettings();
const { user } = useUser();
useEffect(() => {
if ('serviceWorker' in navigator && user?.id) {
@ -16,33 +14,12 @@ const ServiceWorkerSetup = () => {
'[SW] Registration successful, scope is:',
registration.scope
);
if (currentSettings.enablePushRegistration) {
const sub = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: currentSettings.vapidPublic,
});
const parsedSub = JSON.parse(JSON.stringify(sub));
if (parsedSub.keys.p256dh && parsedSub.keys.auth) {
await axios.post('/api/v1/user/registerPushSubscription', {
endpoint: parsedSub.endpoint,
p256dh: parsedSub.keys.p256dh,
auth: parsedSub.keys.auth,
});
}
}
})
.catch(function (error) {
console.log('[SW] Service worker registration failed, error:', error);
});
}
}, [
user,
currentSettings.vapidPublic,
currentSettings.enablePushRegistration,
]);
}, [user]);
return null;
};

@ -1,123 +0,0 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector, {
ALL_NOTIFICATIONS,
} from '@app/components/NotificationTypeSelector';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
const messages = defineMessages({
webpushsettingssaved: 'Web push notification settings saved successfully!',
webpushsettingsfailed: 'Web push notification settings failed to save.',
});
const UserWebPushSettings = () => {
const intl = useIntl();
const { addToast } = useToasts();
const router = useRouter();
const { user } = useUser({ id: Number(router.query.userId) });
const {
data,
error,
mutate: revalidate,
} = useSWR<UserSettingsNotificationsResponse>(
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
);
if (!data && !error) {
return <LoadingSpinner />;
}
return (
<Formik
initialValues={{
types: data?.notificationTypes.webpush ?? ALL_NOTIFICATIONS,
}}
enableReinitialize
onSubmit={async (values) => {
try {
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
pgpKey: data?.pgpKey,
discordId: data?.discordId,
pushbulletAccessToken: data?.pushbulletAccessToken,
pushoverApplicationToken: data?.pushoverApplicationToken,
pushoverUserKey: data?.pushoverUserKey,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
notificationTypes: {
webpush: values.types,
},
});
mutate('/api/v1/settings/public');
addToast(intl.formatMessage(messages.webpushsettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
addToast(intl.formatMessage(messages.webpushsettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally {
revalidate();
}
}}
>
{({
errors,
touched,
isSubmitting,
isValid,
values,
setFieldValue,
setFieldTouched,
}) => {
return (
<Form className="section">
<NotificationTypeSelector
user={user}
currentTypes={values.types}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
>
<ArrowDownOnSquareIcon />
<span>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</span>
</Button>
</span>
</div>
</div>
</Form>
);
}}
</Formik>
);
};
export default UserWebPushSettings;

@ -0,0 +1,107 @@
import ConfirmButton from '@app/components/Common/ConfirmButton';
import globalMessages from '@app/i18n/globalMessages';
import {
ComputerDesktopIcon,
DevicePhoneMobileIcon,
TrashIcon,
} from '@heroicons/react/24/solid';
import { defineMessages, useIntl } from 'react-intl';
import { UAParser } from 'ua-parser-js';
interface DeviceItemProps {
disablePushNotifications: (p256dh: string) => void;
device: {
endpoint: string;
p256dh: string;
auth: string;
userAgent: string;
createdAt: Date;
};
}
const messages = defineMessages({
operatingsystem: 'Operating System',
browser: 'Browser',
engine: 'Engine',
deletesubscription: 'Delete Subscription',
});
const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
const intl = useIntl();
return (
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
<div className="relative flex w-full flex-col justify-between overflow-hidden sm:flex-row">
<div className="relative z-10 flex w-full items-center overflow-hidden pl-4 pr-4 sm:pr-0 xl:w-7/12 2xl:w-2/3">
<div className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105">
{UAParser(device.userAgent).device.type === 'mobile' ? (
<DevicePhoneMobileIcon />
) : (
<ComputerDesktopIcon />
)}
</div>
<div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
<div className="pt-0.5 text-xs font-medium text-white sm:pt-1">
{device.createdAt
? intl.formatDate(device.createdAt, {
year: 'numeric',
month: 'long',
day: 'numeric',
})
: 'Unknown'}
</div>
<div className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl">
{device.userAgent
? UAParser(device.userAgent).device.model
: 'Unknown'}
</div>
</div>
</div>
<div className="z-10 mt-4 ml-4 flex w-full flex-col justify-center overflow-hidden pr-4 text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
<div className="card-field">
<span className="card-field-name">
{intl.formatMessage(messages.operatingsystem)}
</span>
<span className="flex truncate text-sm text-gray-300">
{device.userAgent
? UAParser(device.userAgent).os.name
: 'Unknown'}
</span>
</div>
<div className="card-field">
<span className="card-field-name">
{intl.formatMessage(messages.browser)}
</span>
<span className="flex truncate text-sm text-gray-300">
{device.userAgent
? UAParser(device.userAgent).browser.name
: 'Unknown'}
</span>
</div>
<div className="card-field">
<span className="card-field-name">
{intl.formatMessage(messages.engine)}
</span>
<span className="flex truncate text-sm text-gray-300">
{device.userAgent
? UAParser(device.userAgent).engine.name
: 'Unknown'}
</span>
</div>
</div>
</div>
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
<ConfirmButton
onClick={() => disablePushNotifications(device.p256dh)}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<TrashIcon />
<span>{intl.formatMessage(messages.deletesubscription)}</span>
</ConfirmButton>
</div>
</div>
);
};
export default DeviceItem;

@ -0,0 +1,338 @@
import Alert from '@app/components/Common/Alert';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector, {
ALL_NOTIFICATIONS,
} from '@app/components/NotificationTypeSelector';
import DeviceItem from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/DeviceItem';
import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
import {
CloudArrowDownIcon,
CloudArrowUpIcon,
} from '@heroicons/react/24/solid';
import type { UserPushSubscription } from '@server/entity/UserPushSubscription';
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
import axios from 'axios';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
const messages = defineMessages({
webpushsettingssaved: 'Web push notification settings saved successfully!',
webpushsettingsfailed: 'Web push notification settings failed to save.',
enablewebpush: 'Enable web push',
disablewebpush: 'Disable web push',
managedevices: 'Manage Devices',
type: 'type',
created: 'Created',
device: 'Device',
subscriptiondeleted: 'Subscription deleted.',
subscriptiondeleteerror:
'Something went wrong while deleting the user subscription.',
nodevicestoshow: 'You have no web push subscriptions to show.',
webpushhasbeenenabled: 'Web push has been enabled.',
webpushhasbeendisabled: 'Web push has been disabled.',
enablingwebpusherror: 'Something went wrong while enabling web push.',
disablingwebpusherror: 'Something went wrong while disabling web push.',
});
const UserWebPushSettings = () => {
const intl = useIntl();
const { addToast } = useToasts();
const router = useRouter();
const { user } = useUser({ id: Number(router.query.userId) });
const { currentSettings } = useSettings();
const [webPushEnabled, setWebPushEnabled] = useState(false);
const {
data,
error,
mutate: revalidate,
} = useSWR<UserSettingsNotificationsResponse>(
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
);
const { data: dataDevices, mutate: revalidateDevices } = useSWR<
{
endpoint: string;
p256dh: string;
auth: string;
userAgent: string;
createdAt: Date;
}[]
>(`/api/v1/user/${user?.id}/pushSubscriptions`, { revalidateOnMount: true });
// Subscribes to the push manager
// Will only add to the database if subscribing for the first time
const enablePushNotifications = () => {
if ('serviceWorker' in navigator && user?.id) {
navigator.serviceWorker
.getRegistration('/sw.js')
.then(async (registration) => {
if (currentSettings.enablePushRegistration) {
const sub = await registration?.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: currentSettings.vapidPublic,
});
const parsedSub = JSON.parse(JSON.stringify(sub));
if (parsedSub.keys.p256dh && parsedSub.keys.auth) {
await axios.post('/api/v1/user/registerPushSubscription', {
endpoint: parsedSub.endpoint,
p256dh: parsedSub.keys.p256dh,
auth: parsedSub.keys.auth,
userAgent: navigator.userAgent,
});
setWebPushEnabled(true);
addToast(intl.formatMessage(messages.webpushhasbeenenabled), {
appearance: 'success',
autoDismiss: true,
});
}
}
})
.catch(function () {
addToast(intl.formatMessage(messages.enablingwebpusherror), {
autoDismiss: true,
appearance: 'error',
});
})
.finally(function () {
revalidateDevices();
});
}
};
// Unsubscribes from the push manager
// Deletes/disables corresponding push subscription from database
const disablePushNotifications = async (p256dh?: string) => {
if ('serviceWorker' in navigator && user?.id) {
navigator.serviceWorker.getRegistration('/sw.js').then((registration) => {
registration?.pushManager
.getSubscription()
.then(async (subscription) => {
const parsedSub = JSON.parse(JSON.stringify(subscription));
await axios.delete(
`/api/v1/user/${user?.id}/pushSubscription/${
p256dh ? p256dh : parsedSub.keys.p256dh
}`
);
if (subscription && (p256dh === parsedSub.keys.p256dh || !p256dh)) {
subscription.unsubscribe();
setWebPushEnabled(false);
}
addToast(
intl.formatMessage(
p256dh
? messages.subscriptiondeleted
: messages.webpushhasbeendisabled
),
{
autoDismiss: true,
appearance: 'success',
}
);
})
.catch(function () {
addToast(
intl.formatMessage(
p256dh
? messages.subscriptiondeleteerror
: messages.disablingwebpusherror
),
{
autoDismiss: true,
appearance: 'error',
}
);
})
.finally(function () {
revalidateDevices();
});
});
}
};
// Checks our current subscription on page load
// Will set the web push state to true if subscribed
useEffect(() => {
if ('serviceWorker' in navigator && user?.id) {
navigator.serviceWorker
.getRegistration('/sw.js')
.then(async (registration) => {
await registration?.pushManager
.getSubscription()
.then(async (subscription) => {
if (subscription) {
const parsedKey = JSON.parse(JSON.stringify(subscription));
const currentUserPushSub =
await axios.get<UserPushSubscription>(
`/api/v1/user/${user.id}/pushSubscription/${parsedKey.keys.p256dh}`
);
if (currentUserPushSub.data.p256dh !== parsedKey.keys.p256dh) {
return;
}
setWebPushEnabled(true);
} else {
setWebPushEnabled(false);
}
});
})
.catch(function (error) {
setWebPushEnabled(false);
// eslint-disable-next-line no-console
console.log(
'[SW] Failure retrieving push manager subscription, error:',
error
);
});
}
}, [user?.id]);
if (!data && !error) {
return <LoadingSpinner />;
}
return (
<>
<Formik
initialValues={{
types: data?.notificationTypes.webpush ?? ALL_NOTIFICATIONS,
}}
enableReinitialize
onSubmit={async (values) => {
try {
await axios.post(
`/api/v1/user/${user?.id}/settings/notifications`,
{
pgpKey: data?.pgpKey,
discordId: data?.discordId,
pushbulletAccessToken: data?.pushbulletAccessToken,
pushoverApplicationToken: data?.pushoverApplicationToken,
pushoverUserKey: data?.pushoverUserKey,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
notificationTypes: {
webpush: values.types,
},
}
);
mutate('/api/v1/settings/public');
addToast(intl.formatMessage(messages.webpushsettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
addToast(intl.formatMessage(messages.webpushsettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally {
revalidate();
}
}}
>
{({
errors,
touched,
isSubmitting,
isValid,
values,
setFieldValue,
setFieldTouched,
}) => {
return (
<Form className="section">
<NotificationTypeSelector
user={user}
currentTypes={values.types}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<Button
buttonType={`${webPushEnabled ? 'danger' : 'primary'}`}
type="button"
onClick={() =>
webPushEnabled
? disablePushNotifications()
: enablePushNotifications()
}
>
{webPushEnabled ? (
<CloudArrowDownIcon />
) : (
<CloudArrowUpIcon />
)}
<span>
{webPushEnabled
? intl.formatMessage(messages.disablewebpush)
: intl.formatMessage(messages.enablewebpush)}
</span>
</Button>
</span>
<span className="ml-3 inline-flex rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
>
<ArrowDownOnSquareIcon />
<span>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</span>
</Button>
</span>
</div>
</div>
</Form>
);
}}
</Formik>
<div className="mt-10 mb-6">
<h3 className="heading">
{intl.formatMessage(messages.managedevices)}
</h3>
<div className="section">
{dataDevices?.length ? (
dataDevices?.map((device, index) => (
<div className="py-2" key={`device-list-${index}`}>
<DeviceItem
key={index}
disablePushNotifications={disablePushNotifications}
device={device}
/>
</div>
))
) : (
<>
<Alert
title={intl.formatMessage(messages.nodevicestoshow)}
type="info"
/>
</>
)}
</div>
</div>
</>
);
};
export default UserWebPushSettings;

@ -1106,6 +1106,25 @@
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!",
"components.UserProfile.UserSettings.UserGeneralSettings.user": "User",
"components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "You must provide a valid Discord user ID",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "Browser",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "Created",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "Delete Subscription",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "Device",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "Disable web push",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "Something went wrong while disabling web push.",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "Enable web push",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "Something went wrong while enabling web push.",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "Engine",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "Manage Devices",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "You have no web push subscriptions to show.",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "Operating System",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "Subscription deleted.",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "Something went wrong while deleting the user subscription.",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "type",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "Web push has been disabled.",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "Web push has been enabled.",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "Web push notification settings failed to save.",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "Web push notification settings saved successfully!",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Device Default",
"components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID",
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your user account",
@ -1142,8 +1161,6 @@
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "You must provide a valid user or group key",
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "You must provide a valid chat ID",
"components.UserProfile.UserSettings.UserNotificationSettings.webpush": "Web Push",
"components.UserProfile.UserSettings.UserNotificationSettings.webpushsettingsfailed": "Web push notification settings failed to save.",
"components.UserProfile.UserSettings.UserNotificationSettings.webpushsettingssaved": "Web push notification settings saved successfully!",
"components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Confirm Password",
"components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Current Password",
"components.UserProfile.UserSettings.UserPasswordChange.newpassword": "New Password",
@ -1224,6 +1241,7 @@
"i18n.saving": "Saving…",
"i18n.settings": "Settings",
"i18n.showingresults": "Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results",
"i18n.specials": "Specials",
"i18n.status": "Status",
"i18n.test": "Test",
"i18n.testing": "Testing…",

@ -4040,6 +4040,11 @@
"@types/express" "*"
"@types/serve-static" "*"
"@types/ua-parser-js@^0.7.36":
version "0.7.36"
resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190"
integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==
"@types/unist@*", "@types/unist@^2.0.0":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
@ -13332,6 +13337,11 @@ typescript@^4.0, typescript@^4.6.4, typescript@^4.7:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==
ua-parser-js@^1.0.35:
version "1.0.35"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.35.tgz#c4ef44343bc3db0a3cbefdf21822f1b1fc1ab011"
integrity sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==
uc.micro@^1.0.1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"

Loading…
Cancel
Save