From daba80177a7bade502b9acc32828f6b89337bba0 Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Mon, 3 Apr 2023 20:36:59 -0700 Subject: [PATCH] feat: :sparkles: push notifs button Co-authored-by: Tamania --- locales/en-US.yml | 1 + locales/ja-JP.yml | 1 + ...6634-whetherPushNotifyToSendReadMessage.js | 11 ++ .../src/models/entities/sw-subscription.ts | 5 + packages/backend/src/server/api/endpoints.ts | 4 + .../src/server/api/endpoints/sw/register.ts | 26 ++- .../api/endpoints/sw/show-registration.ts | 55 ++++++ .../src/server/api/endpoints/sw/unregister.ts | 6 +- .../api/endpoints/sw/update-registration.ts | 44 +++++ .../backend/src/services/push-notification.ts | 7 + .../MkPushNotificationAllowButton.vue | 171 ++++++++++++++++++ .../src/components/MkTutorialDialog.vue | 2 + .../src/pages/settings/notifications.vue | 32 ++++ packages/client/src/scripts/initialize-sw.ts | 56 ------ 14 files changed, 359 insertions(+), 62 deletions(-) create mode 100644 packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js create mode 100644 packages/backend/src/server/api/endpoints/sw/show-registration.ts create mode 100644 packages/backend/src/server/api/endpoints/sw/update-registration.ts create mode 100644 packages/client/src/components/MkPushNotificationAllowButton.vue diff --git a/locales/en-US.yml b/locales/en-US.yml index f4913e9041..0a0f86a106 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -12,6 +12,7 @@ fetchingAsApObject: "Fetching from the Fediverse" ok: "OK" gotIt: "Got it!" cancel: "Cancel" +noThankYou: "No thank you" enterUsername: "Enter username" renotedBy: "Boosted by {user}" noNotes: "No posts" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 64d804928e..29e2416222 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -12,6 +12,7 @@ fetchingAsApObject: "連合宇宙から取得中" ok: "OK" gotIt: "わかった!" cancel: "キャンセル" +noThankYou: "やめておく" enterUsername: "ユーザー名を入力" renotedBy: "{user}がブースト" noNotes: "投稿はありません" diff --git a/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js b/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js new file mode 100644 index 0000000000..2265b00617 --- /dev/null +++ b/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js @@ -0,0 +1,11 @@ +export class whetherPushNotifyToSendReadMessage1669138716634 { + name = 'whetherPushNotifyToSendReadMessage1669138716634' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "sw_subscription" ADD "sendReadMessage" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "sw_subscription" DROP COLUMN "sendReadMessage"`); + } +} diff --git a/packages/backend/src/models/entities/sw-subscription.ts b/packages/backend/src/models/entities/sw-subscription.ts index 26891c1ce7..8f18688eab 100644 --- a/packages/backend/src/models/entities/sw-subscription.ts +++ b/packages/backend/src/models/entities/sw-subscription.ts @@ -41,4 +41,9 @@ export class SwSubscription { length: 128, }) public publickey: string; + + @Column('boolean', { + default: false, + }) + public sendReadMessage: boolean; } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 920f871995..57a3ce4dc2 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -290,6 +290,8 @@ import * as ep___resetDb from "./endpoints/reset-db.js"; import * as ep___resetPassword from "./endpoints/reset-password.js"; import * as ep___serverInfo from "./endpoints/server-info.js"; import * as ep___stats from "./endpoints/stats.js"; +import * as ep___sw_show_registration from './endpoints/sw/show-registration.js'; +import * as ep___sw_update_registration from './endpoints/sw/update-registration.js'; import * as ep___sw_register from "./endpoints/sw/register.js"; import * as ep___sw_unregister from "./endpoints/sw/unregister.js"; import * as ep___test from "./endpoints/test.js"; @@ -637,6 +639,8 @@ const eps = [ ["stats", ep___stats], ["sw/register", ep___sw_register], ["sw/unregister", ep___sw_unregister], + ['sw/show-registration', ep___sw_show_registration], + ['sw/update-registration', ep___sw_update_registration], ["test", ep___test], ["username/available", ep___username_available], ["users", ep___users], diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index 7218b0d50a..d2b805d974 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -26,6 +26,18 @@ export const meta = { optional: false, nullable: true, }, + userId: { + type: 'string', + optional: false, nullable: false, + }, + endpoint: { + type: 'string', + optional: false, nullable: false, + }, + sendReadMessage: { + type: 'boolean', + optional: false, nullable: false, + }, }, }, } as const; @@ -36,14 +48,15 @@ export const paramDef = { endpoint: { type: "string" }, auth: { type: "string" }, publickey: { type: "string" }, + sendReadMessage: { type: 'boolean', default: false }, }, required: ["endpoint", "auth", "publickey"], } as const; -export default define(meta, paramDef, async (ps, user) => { +export default define(meta, paramDef, async (ps, me) => { // if already subscribed const exist = await SwSubscriptions.findOneBy({ - userId: user.id, + userId: me.id, endpoint: ps.endpoint, auth: ps.auth, publickey: ps.publickey, @@ -55,20 +68,27 @@ export default define(meta, paramDef, async (ps, user) => { return { state: "already-subscribed" as const, key: instance.swPublicKey, + userId: me.id, + endpoint: exist.endpoint, + sendReadMessage: exist.sendReadMessage, }; } await SwSubscriptions.insert({ id: genId(), createdAt: new Date(), - userId: user.id, + userId: me.id, endpoint: ps.endpoint, auth: ps.auth, publickey: ps.publickey, + sendReadMessage: ps.sendReadMessage, }); return { state: "subscribed" as const, key: instance.swPublicKey, + userId: me.id, + endpoint: ps.endpoint, + sendReadMessage: ps.sendReadMessage, }; }); diff --git a/packages/backend/src/server/api/endpoints/sw/show-registration.ts b/packages/backend/src/server/api/endpoints/sw/show-registration.ts new file mode 100644 index 0000000000..25eb53f527 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/sw/show-registration.ts @@ -0,0 +1,55 @@ +import { SwSubscriptions } from '@/models/index.js'; +import define from "../../define.js"; + +export const meta = { + tags: ['account'], + + requireCredential: true, + + description: 'Check push notification registration exists.', + + res: { + type: 'object', + optional: false, nullable: true, + properties: { + userId: { + type: 'string', + optional: false, nullable: false, + }, + endpoint: { + type: 'string', + optional: false, nullable: false, + }, + sendReadMessage: { + type: 'boolean', + optional: false, nullable: false, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + endpoint: { type: 'string' }, + }, + required: ['endpoint'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, me) => { + const exist = await SwSubscriptions.findOneBy({ + userId: me.id, + endpoint: ps.endpoint, + }); + + if (exist != null) { + return { + userId: exist.userId, + endpoint: exist.endpoint, + sendReadMessage: exist.sendReadMessage, + }; + } + + return null; +}); diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index b025630e4b..e2a40f51cb 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -4,7 +4,7 @@ import define from "../../define.js"; export const meta = { tags: ["account"], - requireCredential: true, + requireCredential: false, description: "Unregister from receiving push notifications.", } as const; @@ -17,9 +17,9 @@ export const paramDef = { required: ["endpoint"], } as const; -export default define(meta, paramDef, async (ps, user) => { +export default define(meta, paramDef, async (ps, me) => { await SwSubscriptions.delete({ - userId: user.id, + ...(me ? { userId: me.id } : {}), endpoint: ps.endpoint, }); }); diff --git a/packages/backend/src/server/api/endpoints/sw/update-registration.ts b/packages/backend/src/server/api/endpoints/sw/update-registration.ts new file mode 100644 index 0000000000..0b0a56d499 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/sw/update-registration.ts @@ -0,0 +1,44 @@ +import { SwSubscriptions } from "@/models/index.js"; +import define from "../../define.js"; + +export const meta = { + tags: ["account"], + + requireCredential: true, + + description: "Unregister from receiving push notifications.", +} as const; + +export const paramDef = { + type: "object", + properties: { + endpoint: { type: "string" }, + sendReadMessage: { type: 'boolean' }, + }, + required: ["endpoint"], +} as const; + +export default define(meta, paramDef, async (ps, me) => { + const swSubscription = await SwSubscriptions.findOneBy({ + userId: me.id, + endpoint: ps.endpoint, + }); + + if (swSubscription === null) { + throw new Error("No such registration"); + } + + if (ps.sendReadMessage !== undefined) { + swSubscription.sendReadMessage = ps.sendReadMessage; + } + + await SwSubscriptions.update(swSubscription.id, { + sendReadMessage: swSubscription.sendReadMessage, + }); + + return { + userId: swSubscription.userId, + endpoint: swSubscription.endpoint, + sendReadMessage: swSubscription.sendReadMessage, + }; +}); diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts index 0e51ad9675..a3abaf769c 100644 --- a/packages/backend/src/services/push-notification.ts +++ b/packages/backend/src/services/push-notification.ts @@ -63,6 +63,13 @@ export async function pushNotification( }); for (const subscription of subscriptions) { + if ([ + 'readNotifications', + 'readAllNotifications', + 'readAllMessagingMessages', + 'readAllMessagingMessagesOfARoom', + ].includes(type) && !subscription.sendReadMessage) continue; + const pushSubscription = { endpoint: subscription.endpoint, keys: { diff --git a/packages/client/src/components/MkPushNotificationAllowButton.vue b/packages/client/src/components/MkPushNotificationAllowButton.vue new file mode 100644 index 0000000000..b98c814f24 --- /dev/null +++ b/packages/client/src/components/MkPushNotificationAllowButton.vue @@ -0,0 +1,171 @@ + + + diff --git a/packages/client/src/components/MkTutorialDialog.vue b/packages/client/src/components/MkTutorialDialog.vue index c077d3fc5f..77cc4de6fb 100644 --- a/packages/client/src/components/MkTutorialDialog.vue +++ b/packages/client/src/components/MkTutorialDialog.vue @@ -106,6 +106,7 @@

{{ i18n.ts._tutorial.step6_4 }}

+ @@ -122,6 +123,7 @@ import MkButton from '@/components/MkButton.vue'; import XFeaturedUsers from '@/pages/explore.users.vue'; import XPostForm from '@/components/MkPostForm.vue'; import MkSparkle from '@/components/MkSparkle.vue'; +import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; import { $i } from '@/account'; diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue index b0bf970bca..586da9e154 100644 --- a/packages/client/src/pages/settings/notifications.vue +++ b/packages/client/src/pages/settings/notifications.vue @@ -6,6 +6,21 @@ {{ i18n.ts.markAsReadAllUnreadNotes }} {{ i18n.ts.markAsReadAllTalkMessages }} + + + +
+ + + + + +
+
@@ -19,6 +34,11 @@ import * as os from '@/os'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; + +let allowButton = $shallowRef>(); +let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer); +let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false); async function readAllUnreadNotes() { await os.api('i/read-all-unread-notes'); @@ -49,6 +69,18 @@ function configure() { }, 'closed'); } +function onChangeSendReadMessage(v: boolean) { + if (!pushRegistrationInServer) return; + + os.apiWithDialog('sw/update-registration', { + endpoint: pushRegistrationInServer.endpoint, + sendReadMessage: v, + }).then(res => { + if (!allowButton) return; + allowButton.pushRegistrationInServer = res; + }); +} + const headerActions = $computed(() => []); const headerTabs = $computed(() => []); diff --git a/packages/client/src/scripts/initialize-sw.ts b/packages/client/src/scripts/initialize-sw.ts index 74f0e9b446..737f86515a 100644 --- a/packages/client/src/scripts/initialize-sw.ts +++ b/packages/client/src/scripts/initialize-sw.ts @@ -1,6 +1,3 @@ -import { instance } from "@/instance"; -import { $i } from "@/account"; -import { api } from "@/os"; import { lang } from "@/config"; export async function initializeSw() { @@ -12,58 +9,5 @@ export async function initializeSw() { msg: "initialize", lang, }); - - if (instance.swPublickey && "PushManager" in window && $i && $i.token) { - // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters - registration.pushManager - .subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(instance.swPublickey), - }) - .then((subscription) => { - function encode(buffer: ArrayBuffer | null) { - return btoa( - String.fromCharCode.apply(null, new Uint8Array(buffer)), - ); - } - - // Register - api("sw/register", { - endpoint: subscription.endpoint, - auth: encode(subscription.getKey("auth")), - publickey: encode(subscription.getKey("p256dh")), - }); - }) - // When subscribe failed - .catch(async (err: Error) => { - // 通知が許可されていなかったとき - if (err.name === "NotAllowedError") { - return; - } - - // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが - // 既に存在していることが原因でエラーになった可能性があるので、 - // そのサブスクリプションを解除しておく - const subscription = await registration.pushManager.getSubscription(); - if (subscription) subscription.unsubscribe(); - }); - } }); } - -/** - * Convert the URL safe base64 string to a Uint8Array - * @param base64String base64 string - */ -function urlBase64ToUint8Array(base64String: string): Uint8Array { - const padding = "=".repeat((4 - (base64String.length % 4)) % 4); - const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); - - const rawData = window.atob(base64); - const outputArray = new Uint8Array(rawData.length); - - for (let i = 0; i < rawData.length; ++i) { - outputArray[i] = rawData.charCodeAt(i); - } - return outputArray; -}