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 * ✌️ * implement subscribe push notification button to tutorial * check-exists→show-registration * add column sendReadMessage * fix migration file * sw api * change PushNotificationService * wip * ✌️ * fix tutorial footer flex
This commit is contained in:
parent
96dda253b0
commit
4ecc42744c
14 changed files with 483 additions and 106 deletions
|
@ -13,6 +13,7 @@ fetchingAsApObject: "連合に照会中"
|
||||||
ok: "OK"
|
ok: "OK"
|
||||||
gotIt: "わかった"
|
gotIt: "わかった"
|
||||||
cancel: "キャンセル"
|
cancel: "キャンセル"
|
||||||
|
noThankYou: "やめておく"
|
||||||
enterUsername: "ユーザー名を入力"
|
enterUsername: "ユーザー名を入力"
|
||||||
renotedBy: "{user}がRenote"
|
renotedBy: "{user}がRenote"
|
||||||
noNotes: "ノートはありません"
|
noNotes: "ノートはありません"
|
||||||
|
@ -898,6 +899,13 @@ navbar: "ナビゲーションバー"
|
||||||
shuffle: "シャッフル"
|
shuffle: "シャッフル"
|
||||||
account: "アカウント"
|
account: "アカウント"
|
||||||
move: "移動"
|
move: "移動"
|
||||||
|
pushNotification: "プッシュ通知"
|
||||||
|
subscribePushNotification: "プッシュ通知を有効化"
|
||||||
|
unsubscribePushNotification: "プッシュ通知を停止する"
|
||||||
|
pushNotificationAlreadySubscribed: "プッシュ通知は有効です"
|
||||||
|
pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に非対応"
|
||||||
|
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
|
||||||
|
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||||
|
@ -1235,6 +1243,9 @@ _tutorial:
|
||||||
step7_1: "これで、Misskeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
|
step7_1: "これで、Misskeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
|
||||||
step7_2: "もっとMisskeyについて知りたいときは、{help}を見てみてください。"
|
step7_2: "もっとMisskeyについて知りたいときは、{help}を見てみてください。"
|
||||||
step7_3: "では、Misskeyをお楽しみください🚀"
|
step7_3: "では、Misskeyをお楽しみください🚀"
|
||||||
|
step8_1: "最後に、プッシュ通知を有効化してみませんか?"
|
||||||
|
step8_2: "プッシュ通知を受け取ることで、Misskeyを開いていない時にもリアクションやフォロー、メンションなどに気づけます。"
|
||||||
|
step8_3: "通知の設定は後から変更できます。"
|
||||||
|
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "既に設定は完了しています。"
|
alreadyRegistered: "既に設定は完了しています。"
|
||||||
|
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -69,6 +69,14 @@ export class PushNotificationService {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
|
// Continue if sendReadMessage is false
|
||||||
|
if ([
|
||||||
|
'readNotifications',
|
||||||
|
'readAllNotifications',
|
||||||
|
'readAllMessagingMessages',
|
||||||
|
'readAllMessagingMessagesOfARoom',
|
||||||
|
].includes(type) && !subscription.sendReadMessage) continue;
|
||||||
|
|
||||||
const pushSubscription = {
|
const pushSubscription = {
|
||||||
endpoint: subscription.endpoint,
|
endpoint: subscription.endpoint,
|
||||||
keys: {
|
keys: {
|
||||||
|
|
|
@ -34,4 +34,9 @@ export class SwSubscription {
|
||||||
length: 128,
|
length: 128,
|
||||||
})
|
})
|
||||||
public publickey: string;
|
public publickey: string;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public sendReadMessage: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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___resetPassword from './endpoints/reset-password.js';
|
||||||
import * as ep___serverInfo from './endpoints/server-info.js';
|
import * as ep___serverInfo from './endpoints/server-info.js';
|
||||||
import * as ep___stats from './endpoints/stats.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_register from './endpoints/sw/register.js';
|
||||||
import * as ep___sw_unregister from './endpoints/sw/unregister.js';
|
import * as ep___sw_unregister from './endpoints/sw/unregister.js';
|
||||||
import * as ep___test from './endpoints/test.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 $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default };
|
||||||
const $serverInfo: Provider = { provide: 'ep:server-info', useClass: ep___serverInfo.default };
|
const $serverInfo: Provider = { provide: 'ep:server-info', useClass: ep___serverInfo.default };
|
||||||
const $stats: Provider = { provide: 'ep:stats', useClass: ep___stats.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_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 $sw_unregister: Provider = { provide: 'ep:sw/unregister', useClass: ep___sw_unregister.default };
|
||||||
const $test: Provider = { provide: 'ep:test', useClass: ep___test.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,
|
$resetPassword,
|
||||||
$serverInfo,
|
$serverInfo,
|
||||||
$stats,
|
$stats,
|
||||||
|
$sw_show_registration,
|
||||||
|
$sw_update_registration,
|
||||||
$sw_register,
|
$sw_register,
|
||||||
$sw_unregister,
|
$sw_unregister,
|
||||||
$test,
|
$test,
|
||||||
|
|
|
@ -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___resetPassword from './endpoints/reset-password.js';
|
||||||
import * as ep___serverInfo from './endpoints/server-info.js';
|
import * as ep___serverInfo from './endpoints/server-info.js';
|
||||||
import * as ep___stats from './endpoints/stats.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_register from './endpoints/sw/register.js';
|
||||||
import * as ep___sw_unregister from './endpoints/sw/unregister.js';
|
import * as ep___sw_unregister from './endpoints/sw/unregister.js';
|
||||||
import * as ep___test from './endpoints/test.js';
|
import * as ep___test from './endpoints/test.js';
|
||||||
|
@ -585,6 +587,8 @@ const eps = [
|
||||||
['reset-password', ep___resetPassword],
|
['reset-password', ep___resetPassword],
|
||||||
['server-info', ep___serverInfo],
|
['server-info', ep___serverInfo],
|
||||||
['stats', ep___stats],
|
['stats', ep___stats],
|
||||||
|
['sw/show-registration', ep___sw_show_registration],
|
||||||
|
['sw/update-registration', ep___sw_update_registration],
|
||||||
['sw/register', ep___sw_register],
|
['sw/register', ep___sw_register],
|
||||||
['sw/unregister', ep___sw_unregister],
|
['sw/unregister', ep___sw_unregister],
|
||||||
['test', ep___test],
|
['test', ep___test],
|
||||||
|
|
|
@ -25,6 +25,18 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
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;
|
} as const;
|
||||||
|
@ -35,6 +47,7 @@ export const paramDef = {
|
||||||
endpoint: { type: 'string' },
|
endpoint: { type: 'string' },
|
||||||
auth: { type: 'string' },
|
auth: { type: 'string' },
|
||||||
publickey: { type: 'string' },
|
publickey: { type: 'string' },
|
||||||
|
sendReadMessage: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
required: ['endpoint', 'auth', 'publickey'],
|
required: ['endpoint', 'auth', 'publickey'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -64,6 +77,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
return {
|
return {
|
||||||
state: 'already-subscribed' as const,
|
state: 'already-subscribed' as const,
|
||||||
key: instance.swPublicKey,
|
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,
|
endpoint: ps.endpoint,
|
||||||
auth: ps.auth,
|
auth: ps.auth,
|
||||||
publickey: ps.publickey,
|
publickey: ps.publickey,
|
||||||
|
sendReadMessage: ps.sendReadMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state: 'subscribed' as const,
|
state: 'subscribed' as const,
|
||||||
key: instance.swPublicKey,
|
key: instance.swPublicKey,
|
||||||
|
userId: me.id,
|
||||||
|
endpoint: ps.endpoint,
|
||||||
|
sendReadMessage: ps.sendReadMessage,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['account'],
|
tags: ['account'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: false,
|
||||||
|
|
||||||
description: 'Unregister from receiving push notifications.',
|
description: 'Unregister from receiving push notifications.',
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -28,7 +28,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.swSubscriptionsRepository.delete({
|
await this.swSubscriptionsRepository.delete({
|
||||||
userId: me.id,
|
...(me ? { userId: me.id } : {}),
|
||||||
endpoint: ps.endpoint,
|
endpoint: ps.endpoint,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
167
packages/client/src/components/MkPushNotificationAllowButton.vue
Normal file
167
packages/client/src/components/MkPushNotificationAllowButton.vue
Normal file
|
@ -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>
|
|
@ -6,6 +6,18 @@
|
||||||
<FormLink class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink>
|
<FormLink class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink>
|
||||||
<FormLink class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
|
<FormLink class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
|
||||||
</FormSection>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -15,10 +27,16 @@ import { notificationTypes } from 'misskey-js';
|
||||||
import FormButton from '@/components/MkButton.vue';
|
import FormButton from '@/components/MkButton.vue';
|
||||||
import FormLink from '@/components/form/link.vue';
|
import FormLink from '@/components/form/link.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
|
import FormSwitch from '@/components/form/switch.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
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() {
|
async function readAllUnreadNotes() {
|
||||||
await os.api('i/read-all-unread-notes');
|
await os.api('i/read-all-unread-notes');
|
||||||
|
@ -49,6 +67,18 @@ function configure() {
|
||||||
}, 'closed');
|
}, '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 headerActions = $computed(() => []);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="_card tbkwesmv">
|
<div class="_card">
|
||||||
<div class="_title"><i class="fas fa-info-circle"></i> {{ i18n.ts._tutorial.title }}</div>
|
<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 v-if="tutorial === 0" class="_content">
|
||||||
<div>{{ i18n.ts._tutorial.step1_1 }}</div>
|
<div>{{ i18n.ts._tutorial.step1_1 }}</div>
|
||||||
<div>{{ i18n.ts._tutorial.step1_2 }}</div>
|
<div>{{ i18n.ts._tutorial.step1_2 }}</div>
|
||||||
|
@ -15,7 +26,7 @@
|
||||||
<div>{{ i18n.ts._tutorial.step3_1 }}</div>
|
<div>{{ i18n.ts._tutorial.step3_1 }}</div>
|
||||||
<div>{{ i18n.ts._tutorial.step3_2 }}</div>
|
<div>{{ i18n.ts._tutorial.step3_2 }}</div>
|
||||||
<div>{{ i18n.ts._tutorial.step3_3 }}</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>
|
||||||
<div v-else-if="tutorial === 3" class="_content">
|
<div v-else-if="tutorial === 3" class="_content">
|
||||||
<div>{{ i18n.ts._tutorial.step4_1 }}</div>
|
<div>{{ i18n.ts._tutorial.step4_1 }}</div>
|
||||||
|
@ -32,7 +43,7 @@
|
||||||
</template>
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
<div>{{ i18n.ts._tutorial.step5_3 }}</div>
|
<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>
|
||||||
<div v-else-if="tutorial === 5" class="_content">
|
<div v-else-if="tutorial === 5" class="_content">
|
||||||
<div>{{ i18n.ts._tutorial.step6_1 }}</div>
|
<div>{{ i18n.ts._tutorial.step6_1 }}</div>
|
||||||
|
@ -48,19 +59,20 @@
|
||||||
</I18n>
|
</I18n>
|
||||||
<div>{{ i18n.ts._tutorial.step7_3 }}</div>
|
<div>{{ i18n.ts._tutorial.step7_3 }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="tutorial === 7" class="_content">
|
||||||
<div class="_footer navigation">
|
<div>{{ i18n.ts._tutorial.step8_1 }}</div>
|
||||||
<div class="step">
|
<div>{{ i18n.ts._tutorial.step8_2 }}</div>
|
||||||
<button class="arrow _button" :disabled="tutorial === 0" @click="tutorial--">
|
<small :class="$style.small">{{ i18n.ts._tutorial.step8_3 }}</small>
|
||||||
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -68,53 +80,63 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const tutorialsNumber = 8;
|
||||||
|
|
||||||
const tutorial = computed({
|
const tutorial = computed({
|
||||||
get() { return defaultStore.reactiveState.tutorial.value || 0; },
|
get() { return defaultStore.reactiveState.tutorial.value || 0; },
|
||||||
set(value) { defaultStore.set('tutorial', value); },
|
set(value) { defaultStore.set('tutorial', value); },
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.tbkwesmv {
|
.small {
|
||||||
> ._content {
|
|
||||||
> small {
|
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
&Text {
|
||||||
|
margin: 4px 0;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .navigation {
|
.step {
|
||||||
display: flex;
|
margin-left: auto;
|
||||||
flex-direction: row;
|
|
||||||
align-items: baseline;
|
|
||||||
|
|
||||||
> .step {
|
&Arrow {
|
||||||
> .arrow {
|
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> span {
|
&Number {
|
||||||
margin: 0 4px;
|
font-weight: normal;
|
||||||
|
margin: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .ok {
|
.footer {
|
||||||
margin-left: auto;
|
display: flex;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: right;
|
||||||
|
|
||||||
|
&Item {
|
||||||
|
margin: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import { instance } from '@/instance';
|
|
||||||
import { $i } from '@/account';
|
|
||||||
import { api } from '@/os';
|
|
||||||
import { lang } from '@/config';
|
import { lang } from '@/config';
|
||||||
|
|
||||||
export async function initializeSw() {
|
export async function initializeSw() {
|
||||||
|
@ -12,57 +9,5 @@ export async function initializeSw() {
|
||||||
msg: 'initialize',
|
msg: 'initialize',
|
||||||
lang,
|
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;
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue