summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2022-12-18 01:59:59 +0900
committerGitHub <noreply@github.com>2022-12-18 01:59:59 +0900
commit4ecc42744c3c8b68e38f58bfe03919bf437f137a (patch)
tree094230a6992c05cf39136913d02400fae27931d7
parentfix(server): GitHubログインしようとするとreply.setCookie is not a ... (diff)
downloadsharkey-4ecc42744c3c8b68e38f58bfe03919bf437f137a.tar.gz
sharkey-4ecc42744c3c8b68e38f58bfe03919bf437f137a.tar.bz2
sharkey-4ecc42744c3c8b68e38f58bfe03919bf437f137a.zip
enhance: Implement the toggle to (or not to) close push notifications when notifications or messages are read (#9219)
* create file * wip * fix * wip * tabun dekita * :v: * implement subscribe push notification button to tutorial * check-exists→show-registration * add column sendReadMessage * fix migration file * sw api * change PushNotificationService * wip * :v: * fix tutorial footer flex
-rw-r--r--locales/ja-JP.yml11
-rw-r--r--packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js11
-rw-r--r--packages/backend/src/core/PushNotificationService.ts8
-rw-r--r--packages/backend/src/models/entities/SwSubscription.ts5
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts6
-rw-r--r--packages/backend/src/server/api/endpoints.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/sw/register.ts20
-rw-r--r--packages/backend/src/server/api/endpoints/sw/show-registration.ts66
-rw-r--r--packages/backend/src/server/api/endpoints/sw/unregister.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/sw/update-registration.ts82
-rw-r--r--packages/client/src/components/MkPushNotificationAllowButton.vue167
-rw-r--r--packages/client/src/pages/settings/notifications.vue30
-rw-r--r--packages/client/src/pages/timeline.tutorial.vue116
-rw-r--r--packages/client/src/scripts/initialize-sw.ts55
14 files changed, 481 insertions, 104 deletions
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 3fba36a754..3a53f470e3 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -13,6 +13,7 @@ fetchingAsApObject: "連合に照会中"
ok: "OK"
gotIt: "わかった"
cancel: "キャンセル"
+noThankYou: "やめておく"
enterUsername: "ユーザー名を入力"
renotedBy: "{user}がRenote"
noNotes: "ノートはありません"
@@ -898,6 +899,13 @@ navbar: "ナビゲーションバー"
shuffle: "シャッフル"
account: "アカウント"
move: "移動"
+pushNotification: "プッシュ通知"
+subscribePushNotification: "プッシュ通知を有効化"
+unsubscribePushNotification: "プッシュ通知を停止する"
+pushNotificationAlreadySubscribed: "プッシュ通知は有効です"
+pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に非対応"
+sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
+sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
_sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
@@ -1235,6 +1243,9 @@ _tutorial:
step7_1: "これで、Misskeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
step7_2: "もっとMisskeyについて知りたいときは、{help}を見てみてください。"
step7_3: "では、Misskeyをお楽しみください🚀"
+ step8_1: "最後に、プッシュ通知を有効化してみませんか?"
+ step8_2: "プッシュ通知を受け取ることで、Misskeyを開いていない時にもリアクションやフォロー、メンションなどに気づけます。"
+ step8_3: "通知の設定は後から変更できます。"
_2fa:
alreadyRegistered: "既に設定は完了しています。"
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/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts
index df5284de4b..842cd1a9f8 100644
--- a/packages/backend/src/core/PushNotificationService.ts
+++ b/packages/backend/src/core/PushNotificationService.ts
@@ -69,6 +69,14 @@ export class PushNotificationService {
});
for (const subscription of subscriptions) {
+ // Continue if sendReadMessage is false
+ if ([
+ 'readNotifications',
+ 'readAllNotifications',
+ 'readAllMessagingMessages',
+ 'readAllMessagingMessagesOfARoom',
+ ].includes(type) && !subscription.sendReadMessage) continue;
+
const pushSubscription = {
endpoint: subscription.endpoint,
keys: {
diff --git a/packages/backend/src/models/entities/SwSubscription.ts b/packages/backend/src/models/entities/SwSubscription.ts
index 51b9786e96..0658294983 100644
--- a/packages/backend/src/models/entities/SwSubscription.ts
+++ b/packages/backend/src/models/entities/SwSubscription.ts
@@ -34,4 +34,9 @@ export class SwSubscription {
length: 128,
})
public publickey: string;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public sendReadMessage: boolean;
}
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index e41ed388b4..647f60317a 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -272,6 +272,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';
@@ -588,6 +590,8 @@ const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.defa
const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default };
const $serverInfo: Provider = { provide: 'ep:server-info', useClass: ep___serverInfo.default };
const $stats: Provider = { provide: 'ep:stats', useClass: ep___stats.default };
+const $sw_show_registration: Provider = { provide: 'ep:sw/show-registration', useClass: ep___sw_show_registration.default };
+const $sw_update_registration: Provider = { provide: 'ep:sw/update-registration', useClass: ep___sw_update_registration.default };
const $sw_register: Provider = { provide: 'ep:sw/register', useClass: ep___sw_register.default };
const $sw_unregister: Provider = { provide: 'ep:sw/unregister', useClass: ep___sw_unregister.default };
const $test: Provider = { provide: 'ep:test', useClass: ep___test.default };
@@ -908,6 +912,8 @@ const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.d
$resetPassword,
$serverInfo,
$stats,
+ $sw_show_registration,
+ $sw_update_registration,
$sw_register,
$sw_unregister,
$test,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index b2ab36e070..6d10cb8f35 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -271,6 +271,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';
@@ -585,6 +587,8 @@ const eps = [
['reset-password', ep___resetPassword],
['server-info', ep___serverInfo],
['stats', ep___stats],
+ ['sw/show-registration', ep___sw_show_registration],
+ ['sw/update-registration', ep___sw_update_registration],
['sw/register', ep___sw_register],
['sw/unregister', ep___sw_unregister],
['test', ep___test],
diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts
index ddec877dd4..bfd5de7b00 100644
--- a/packages/backend/src/server/api/endpoints/sw/register.ts
+++ b/packages/backend/src/server/api/endpoints/sw/register.ts
@@ -25,6 +25,18 @@ export const meta = {
type: 'string',
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;
@@ -35,6 +47,7 @@ export const paramDef = {
endpoint: { type: 'string' },
auth: { type: 'string' },
publickey: { type: 'string' },
+ sendReadMessage: { type: 'boolean', default: false },
},
required: ['endpoint', 'auth', 'publickey'],
} as const;
@@ -64,6 +77,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
return {
state: 'already-subscribed' as const,
key: instance.swPublicKey,
+ userId: me.id,
+ endpoint: exist.endpoint,
+ sendReadMessage: exist.sendReadMessage,
};
}
@@ -74,11 +90,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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..bede10be5c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/sw/show-registration.ts
@@ -0,0 +1,66 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { SwSubscriptionsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.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
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.swSubscriptionsRepository)
+ private swSubscriptionsRepository: SwSubscriptionsRepository,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ // if already subscribed
+ const exist = await this.swSubscriptionsRepository.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 5772eeee26..f12b98617d 100644
--- a/packages/backend/src/server/api/endpoints/sw/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts
@@ -6,7 +6,7 @@ import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['account'],
- requireCredential: true,
+ requireCredential: false,
description: 'Unregister from receiving push notifications.',
} as const;
@@ -28,7 +28,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
await this.swSubscriptionsRepository.delete({
- userId: me.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..9f08c8148d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/sw/update-registration.ts
@@ -0,0 +1,82 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { SwSubscriptionsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+ tags: ['account'],
+
+ requireCredential: true,
+
+ description: 'Update push notification registration.',
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ userId: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ endpoint: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ sendReadMessage: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ },
+ },
+ errors: {
+ noSuchRegistration: {
+ message: 'No such registration.',
+ code: 'NO_SUCH_REGISTRATION',
+ id: ' b09d8066-8064-5613-efb6-0e963b21d012',
+ },
+ }
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ endpoint: { type: 'string' },
+ sendReadMessage: { type: 'boolean' },
+ },
+ required: ['endpoint'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.swSubscriptionsRepository)
+ private swSubscriptionsRepository: SwSubscriptionsRepository,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const swSubscription = await this.swSubscriptionsRepository.findOneBy({
+ userId: me.id,
+ endpoint: ps.endpoint,
+ });
+
+ if (swSubscription === null) {
+ throw new ApiError(meta.errors.noSuchRegistration);
+ }
+
+ if (ps.sendReadMessage !== undefined) {
+ swSubscription.sendReadMessage = ps.sendReadMessage;
+ }
+
+ await this.swSubscriptionsRepository.update(swSubscription.id, {
+ sendReadMessage: swSubscription.sendReadMessage,
+ });
+
+ return {
+ userId: swSubscription.userId,
+ endpoint: swSubscription.endpoint,
+ sendReadMessage: swSubscription.sendReadMessage,
+ };
+ });
+ }
+}
diff --git a/packages/client/src/components/MkPushNotificationAllowButton.vue b/packages/client/src/components/MkPushNotificationAllowButton.vue
new file mode 100644
index 0000000000..a762914e64
--- /dev/null
+++ b/packages/client/src/components/MkPushNotificationAllowButton.vue
@@ -0,0 +1,167 @@
+<template>
+<MkButton
+ v-if="supported && !pushRegistrationInServer"
+ type="button"
+ primary
+ :gradate="gradate"
+ :rounded="rounded"
+ :inline="inline"
+ :autofocus="autofocus"
+ :wait="wait"
+ :full="full"
+ @click="subscribe"
+>
+ {{ i18n.ts.subscribePushNotification }}
+</MkButton>
+<MkButton
+ v-else-if="!showOnlyToRegister && ($i ? pushRegistrationInServer : pushSubscription)"
+ type="button"
+ :primary="false"
+ :gradate="gradate"
+ :rounded="rounded"
+ :inline="inline"
+ :autofocus="autofocus"
+ :wait="wait"
+ :full="full"
+ @click="unsubscribe"
+>
+ {{ i18n.ts.unsubscribePushNotification }}
+</MkButton>
+<MkButton v-else-if="$i && pushRegistrationInServer" disabled :rounded="rounded" :inline="inline" :wait="wait" :full="full">
+ {{ i18n.ts.pushNotificationAlreadySubscribed }}
+</MkButton>
+<MkButton v-else-if="!supported" disabled :rounded="rounded" :inline="inline" :wait="wait" :full="full">
+ {{ i18n.ts.pushNotificationNotSupported }}
+</MkButton>
+</template>
+
+<script setup lang="ts">
+import { $i, getAccounts } from '@/account';
+import MkButton from '@/components/MkButton.vue';
+import { instance } from '@/instance';
+import { api, apiWithDialog, promiseDialog } from '@/os';
+import { i18n } from '@/i18n';
+
+defineProps<{
+ primary?: boolean;
+ gradate?: boolean;
+ rounded?: boolean;
+ inline?: boolean;
+ link?: boolean;
+ to?: string;
+ autofocus?: boolean;
+ wait?: boolean;
+ danger?: boolean;
+ full?: boolean;
+ showOnlyToRegister?: boolean;
+}>();
+
+// ServiceWorker registration
+let registration = $ref<ServiceWorkerRegistration | undefined>();
+// If this browser supports push notification
+let supported = $ref(false);
+// If this browser has already subscribed to push notification
+let pushSubscription = $ref<PushSubscription | null>(null);
+let pushRegistrationInServer = $ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>();
+
+function subscribe() {
+ if (!registration || !supported || !instance.swPublickey) return;
+
+ // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
+ return promiseDialog(registration.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: urlBase64ToUint8Array(instance.swPublickey)
+ })
+ .then(async subscription => {
+ pushSubscription = subscription;
+
+ // Register
+ pushRegistrationInServer = await api('sw/register', {
+ endpoint: subscription.endpoint,
+ auth: encode(subscription.getKey('auth')),
+ publickey: encode(subscription.getKey('p256dh'))
+ });
+ }, async err => { // When subscribe failed
+ // 通知が許可されていなかったとき
+ if (err?.name === 'NotAllowedError') {
+ console.info('User denied the notification permission request.');
+ return;
+ }
+
+ // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
+ // 既に存在していることが原因でエラーになった可能性があるので、
+ // そのサブスクリプションを解除しておく
+ // (これは実行されなさそうだけど、おまじない的に古い実装から残してある)
+ await unsubscribe();
+ }), null, null);
+}
+
+async function unsubscribe() {
+ if (!pushSubscription) return;
+
+ const endpoint = pushSubscription.endpoint;
+ const accounts = await getAccounts();
+
+ pushRegistrationInServer = undefined;
+
+ if ($i && accounts.length >= 2) {
+ apiWithDialog('sw/unregister', {
+ i: $i.token,
+ endpoint,
+ });
+ } else {
+ pushSubscription.unsubscribe();
+ apiWithDialog('sw/unregister', {
+ endpoint,
+ });
+ pushSubscription = null;
+ }
+}
+
+function encode(buffer: ArrayBuffer | null) {
+ return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
+}
+
+/**
+ * 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;
+}
+
+navigator.serviceWorker.ready.then(async swr => {
+ registration = swr;
+
+ pushSubscription = await registration.pushManager.getSubscription();
+
+ if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) {
+ supported = true;
+
+ if (pushSubscription) {
+ const res = await api('sw/show-registration', {
+ endpoint: pushSubscription.endpoint,
+ });
+
+ if (res) {
+ pushRegistrationInServer = res;
+ }
+ }
+ }
+});
+
+defineExpose({
+ pushRegistrationInServer: $$(pushRegistrationInServer),
+});
+</script>
diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue
index 5703e0c6b6..77ec567da4 100644
--- a/packages/client/src/pages/settings/notifications.vue
+++ b/packages/client/src/pages/settings/notifications.vue
@@ -6,6 +6,18 @@
<FormLink class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink>
<FormLink class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
</FormSection>
+ <FormSection>
+ <template #label>{{ i18n.ts.pushNotification }}</template>
+ <MkPushNotificationAllowButton ref="allowButton" />
+ <FormSwitch class="_formBlock" :disabled="!pushRegistrationInServer" :model-value="sendReadMessage" @update:modelValue="onChangeSendReadMessage">
+ <template #label>{{ i18n.ts.sendPushNotificationReadMessage }}</template>
+ <template #caption>
+ <I18n :src="i18n.ts.sendPushNotificationReadMessageCaption">
+ <template #emptyPushNotificationMessage>{{ i18n.ts._notification.emptyPushNotificationMessage }}</template>
+ </I18n>
+ </template>
+ </FormSwitch>
+ </FormSection>
</div>
</template>
@@ -15,10 +27,16 @@ import { notificationTypes } from 'misskey-js';
import FormButton from '@/components/MkButton.vue';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
+import FormSwitch from '@/components/form/switch.vue';
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 = $ref<InstanceType<typeof MkPushNotificationAllowButton>>();
+let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer);
+let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false);
async function readAllUnreadNotes() {
await os.api('i/read-all-unread-notes');
@@ -49,6 +67,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/pages/timeline.tutorial.vue b/packages/client/src/pages/timeline.tutorial.vue
index 7f08ccc2a1..9683cc22a5 100644
--- a/packages/client/src/pages/timeline.tutorial.vue
+++ b/packages/client/src/pages/timeline.tutorial.vue
@@ -1,6 +1,17 @@
<template>
-<div class="_card tbkwesmv">
- <div class="_title"><i class="fas fa-info-circle"></i> {{ i18n.ts._tutorial.title }}</div>
+<div class="_card">
+ <div :class="$style.title" class="_title">
+ <div :class="$style.titleText"><i class="fas fa-info-circle"></i> {{ i18n.ts._tutorial.title }}</div>
+ <div :class="$style.step">
+ <button class="_button" :class="$style.stepArrow" :disabled="tutorial === 0" @click="tutorial--">
+ <i class="fas fa-chevron-left"></i>
+ </button>
+ <span :class="$style.stepNumber">{{ tutorial + 1 }} / {{ tutorialsNumber }}</span>
+ <button class="_button" :class="$style.stepArrow" :disabled="tutorial === tutorialsNumber - 1" @click="tutorial++">
+ <i class="fas fa-chevron-right"></i>
+ </button>
+ </div>
+ </div>
<div v-if="tutorial === 0" class="_content">
<div>{{ i18n.ts._tutorial.step1_1 }}</div>
<div>{{ i18n.ts._tutorial.step1_2 }}</div>
@@ -15,7 +26,7 @@
<div>{{ i18n.ts._tutorial.step3_1 }}</div>
<div>{{ i18n.ts._tutorial.step3_2 }}</div>
<div>{{ i18n.ts._tutorial.step3_3 }}</div>
- <small>{{ i18n.ts._tutorial.step3_4 }}</small>
+ <small :class="$style.small">{{ i18n.ts._tutorial.step3_4 }}</small>
</div>
<div v-else-if="tutorial === 3" class="_content">
<div>{{ i18n.ts._tutorial.step4_1 }}</div>
@@ -32,7 +43,7 @@
</template>
</I18n>
<div>{{ i18n.ts._tutorial.step5_3 }}</div>
- <small>{{ i18n.ts._tutorial.step5_4 }}</small>
+ <small :class="$style.small">{{ i18n.ts._tutorial.step5_4 }}</small>
</div>
<div v-else-if="tutorial === 5" class="_content">
<div>{{ i18n.ts._tutorial.step6_1 }}</div>
@@ -48,19 +59,20 @@
</I18n>
<div>{{ i18n.ts._tutorial.step7_3 }}</div>
</div>
+ <div v-else-if="tutorial === 7" class="_content">
+ <div>{{ i18n.ts._tutorial.step8_1 }}</div>
+ <div>{{ i18n.ts._tutorial.step8_2 }}</div>
+ <small :class="$style.small">{{ i18n.ts._tutorial.step8_3 }}</small>
+ </div>
- <div class="_footer navigation">
- <div class="step">
- <button class="arrow _button" :disabled="tutorial === 0" @click="tutorial--">
- <i class="fas fa-chevron-left"></i>
- </button>
- <span>{{ tutorial + 1 }} / 7</span>
- <button class="arrow _button" :disabled="tutorial === 6" @click="tutorial++">
- <i class="fas fa-chevron-right"></i>
- </button>
- </div>
- <MkButton v-if="tutorial === 6" class="ok" primary @click="tutorial = -1"><i class="fas fa-check"></i> {{ i18n.ts.gotIt }}</MkButton>
- <MkButton v-else class="ok" primary @click="tutorial++"><i class="fas fa-check"></i> {{ i18n.ts.next }}</MkButton>
+ <div class="_footer" :class="$style.footer">
+ <template v-if="tutorial === tutorialsNumber - 1">
+ <MkPushNotificationAllowButton :class="$style.footerItem" primary show-only-to-register @click="tutorial = -1" />
+ <MkButton :class="$style.footerItem" :primary="false" @click="tutorial = -1">{{ i18n.ts.noThankYou }}</MkButton>
+ </template>
+ <template v-else>
+ <MkButton :class="$style.footerItem" primary @click="tutorial++"><i class="fas fa-check"></i> {{ i18n.ts.next }}</MkButton>
+ </template>
</div>
</div>
</template>
@@ -68,53 +80,63 @@
<script lang="ts" setup>
import { computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
+import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
+const tutorialsNumber = 8;
+
const tutorial = computed({
get() { return defaultStore.reactiveState.tutorial.value || 0; },
set(value) { defaultStore.set('tutorial', value); },
});
</script>
-<style lang="scss" scoped>
-.tbkwesmv {
- > ._content {
- > small {
- opacity: 0.7;
- }
- }
+<style lang="scss" module>
+.small {
+ opacity: 0.7;
+}
- > .navigation {
- display: flex;
- flex-direction: row;
- align-items: baseline;
+.title {
+ display: flex;
+ flex-wrap: wrap;
- > .step {
- > .arrow {
- padding: 4px;
+ &Text {
+ margin: 4px 0;
+ padding-right: 4px;
+ }
+}
- &:disabled {
- opacity: 0.5;
- }
+.step {
+ margin-left: auto;
- &:first-child {
- padding-right: 8px;
- }
+ &Arrow {
+ padding: 4px;
+ &:disabled {
+ opacity: 0.5;
+ }
+ &:first-child {
+ padding-right: 8px;
+ }
+ &:last-child {
+ padding-left: 8px;
+ }
+ }
- &:last-child {
- padding-left: 8px;
- }
- }
+ &Number {
+ font-weight: normal;
+ margin: 4px;
+ }
+}
- > span {
- margin: 0 4px;
- }
- }
+.footer {
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: row;
+ justify-content: right;
- > .ok {
- margin-left: auto;
- }
+ &Item {
+ margin: 4px;
}
}
</style>
diff --git a/packages/client/src/scripts/initialize-sw.ts b/packages/client/src/scripts/initialize-sw.ts
index 7bacfbdf00..de52f30523 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,57 +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;
-}