sw: なんかもうめっちゃ変えた (#10570)

* sw: なんかいろいろ

* remove debug code

* never renotify

* update changelog.md
This commit is contained in:
tamaina 2023-04-11 14:11:39 +09:00 committed by GitHub
parent f6dc100748
commit 3a90bcc03c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 106 additions and 31 deletions

View file

@ -28,6 +28,13 @@
- アンテナのノート、チャンネルのノート、通知が正常に作成できないことがある問題を修正 - アンテナのノート、チャンネルのノート、通知が正常に作成できないことがある問題を修正
- ストリーミングのLTLチャンネルでサーバー側にエラーログが出るのを修正 - ストリーミングのLTLチャンネルでサーバー側にエラーログが出るのを修正
## Service Worker
- 「通知が既読になったらプッシュ通知を削除する」を復活
* 「プッシュ通知が更新されました」の挙動を変えた(ホストとバージョンを表示するようにし、一定時間後の削除は行わないように)
- プッシュ通知が実績を解除 (achievementEarned) に対応
- プッシュ通知のアクションから既存のクライアントの投稿フォームを開くことになった際の挙動を修正
- たくさんのプッシュ通知を閉じた際、その通知の数だけnotifications/mark-all-as-readを叩くのをやめるように
## 13.11.1 ## 13.11.1
### General ### General

View file

@ -20,6 +20,7 @@ noNotes: "ノートはありません"
noNotifications: "通知はありません" noNotifications: "通知はありません"
instance: "サーバー" instance: "サーバー"
settings: "設定" settings: "設定"
notificationSettings: "通知の設定"
basicSettings: "基本設定" basicSettings: "基本設定"
otherSettings: "その他の設定" otherSettings: "その他の設定"
openInWindow: "ウィンドウで開く" openInWindow: "ウィンドウで開く"
@ -917,8 +918,8 @@ subscribePushNotification: "プッシュ通知を有効化"
unsubscribePushNotification: "プッシュ通知を停止する" unsubscribePushNotification: "プッシュ通知を停止する"
pushNotificationAlreadySubscribed: "プッシュ通知は有効です" pushNotificationAlreadySubscribed: "プッシュ通知は有効です"
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に非対応" pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に非対応"
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する" sendPushNotificationReadMessage: "通知が既読になったらプッシュ通知を削除する"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。" sendPushNotificationReadMessageCaption: "端末の電池消費量が増加する可能性があります。"
windowMaximize: "最大化" windowMaximize: "最大化"
windowMinimize: "最小化" windowMinimize: "最小化"
windowRestore: "元に戻す" windowRestore: "元に戻す"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -66,6 +66,7 @@ export class NotificationService implements OnApplicationShutdown {
@bindThis @bindThis
private postReadAllNotifications(userId: User['id']) { private postReadAllNotifications(userId: User['id']) {
this.globalEventService.publishMainStream(userId, 'readAllNotifications'); this.globalEventService.publishMainStream(userId, 'readAllNotifications');
this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
} }
@bindThis @bindThis

View file

@ -15,6 +15,7 @@ type PushNotificationsTypes = {
antenna: { id: string, name: string }; antenna: { id: string, name: string };
note: Packed<'Note'>; note: Packed<'Note'>;
}; };
'readAllNotifications': undefined;
}; };
// Reduce length because push message servers have character limits // Reduce length because push message servers have character limits
@ -68,6 +69,10 @@ export class PushNotificationService {
}); });
for (const subscription of subscriptions) { for (const subscription of subscriptions) {
if ([
'readAllNotifications',
].includes(type) && !subscription.sendReadMessage) continue;
const pushSubscription = { const pushSubscription = {
endpoint: subscription.endpoint, endpoint: subscription.endpoint,
keys: { keys: {

View file

@ -1,17 +1,18 @@
import { post } from '@/os'; import { api, post } from '@/os';
import { $i, login } from '@/account'; import { $i, login } from '@/account';
import { getAccountFromId } from '@/scripts/get-account-from-id'; import { getAccountFromId } from '@/scripts/get-account-from-id';
import { mainRouter } from '@/router'; import { mainRouter } from '@/router';
import { deepClone } from '@/scripts/clone';
export function swInject() { export function swInject() {
navigator.serviceWorker.addEventListener('message', ev => { navigator.serviceWorker.addEventListener('message', async ev => {
if (_DEV_) { if (_DEV_) {
console.log('sw msg', ev.data); console.log('sw msg', ev.data);
} }
if (ev.data.type !== 'order') return; if (ev.data.type !== 'order') return;
if (ev.data.loginId !== $i?.id) { if (ev.data.loginId && ev.data.loginId !== $i?.id) {
return getAccountFromId(ev.data.loginId).then(account => { return getAccountFromId(ev.data.loginId).then(account => {
if (!account) return; if (!account) return;
return login(account.token, ev.data.url); return login(account.token, ev.data.url);
@ -19,8 +20,18 @@ export function swInject() {
} }
switch (ev.data.order) { switch (ev.data.order) {
case 'post': case 'post': {
return post(ev.data.options); const props = deepClone(ev.data.options);
// プッシュ通知から来たreply,renoteはtruncateBodyが通されているため、
// 完全なノートを取得しなおす
if (props.reply) {
props.reply = await api('notes/show', { noteId: props.reply.id });
}
if (props.renote) {
props.renote = await api('notes/show', { noteId: props.renote.id });
}
return post(props);
}
case 'push': case 'push':
if (mainRouter.currentRoute.value.path === ev.data.url) { if (mainRouter.currentRoute.value.path === ev.data.url) {
return window.scroll({ top: 0, behavior: 'smooth' }); return window.scroll({ top: 0, behavior: 'smooth' });

View file

@ -21,7 +21,7 @@ const iconUrl = (name: BadgeNames) => `/static-assets/tabler-badges/${name}.png`
* 1. Find the icon and download png from https://tabler-icons.io/ * 1. Find the icon and download png from https://tabler-icons.io/
* 2. vips resize ~/Downloads/icon-name.png vipswork.png 0.4; vips scRGB2BW vipswork.png ~/icon-name.png"[compression=9,strip]"; rm vipswork.png; * 2. vips resize ~/Downloads/icon-name.png vipswork.png 0.4; vips scRGB2BW vipswork.png ~/icon-name.png"[compression=9,strip]"; rm vipswork.png;
* 3. mv ~/icon-name.png ~/misskey/packages/backend/assets/tabler-badges/ * 3. mv ~/icon-name.png ~/misskey/packages/backend/assets/tabler-badges/
* 4. Add 'icon-name' to badgeNames * 4. Add 'icon-name' to BadgeNames
* 5. Add `badge: iconUrl('icon-name'),` * 5. Add `badge: iconUrl('icon-name'),`
*/ */
@ -168,14 +168,6 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
}]; }];
} }
case 'pollEnded':
return [t('_notification.pollEnded'), {
body: data.body.note.text || '',
badge: iconUrl('chart-arrows'),
tag: `poll:${data.body.note.id}`,
data,
}];
case 'receiveFollowRequest': case 'receiveFollowRequest':
return [t('_notification.youReceivedFollowRequest'), { return [t('_notification.youReceivedFollowRequest'), {
body: getUserName(data.body.user), body: getUserName(data.body.user),
@ -202,6 +194,14 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
data, data,
}]; }];
case 'achievementEarned':
return [t('_notification.achievementEarned'), {
body: t(`_achievements._types._${data.body.achievement}.title`),
badge: iconUrl('medal'),
data,
tag: `achievement:${data.body.achievement}`,
}];
case 'app': case 'app':
return [data.body.header ?? data.body.body, { return [data.body.header ?? data.body.body, {
body: data.body.header ? data.body.body : '', body: data.body.header ? data.body.body : '',
@ -233,17 +233,29 @@ export async function createEmptyNotification() {
const { t } = i18n; const { t } = i18n;
await globalThis.registration.showNotification( await globalThis.registration.showNotification(
t('_notification.emptyPushNotificationMessage'), (new URL(origin)).host,
{ {
body: `Misskey v${_VERSION_}`,
silent: true, silent: true,
badge: iconUrl('null'), badge: iconUrl('null'),
tag: 'read_notification', tag: 'read_notification',
actions: [
{
action: 'markAllAsRead',
title: t('markAllAsRead'),
},
{
action: 'settings',
title: t('notificationSettings'),
},
],
data: {},
}, },
); );
setTimeout(async () => { setTimeout(async () => {
try { try {
await closeNotificationsByTags(['user_visible_auto_notification', 'read_notification']); await closeNotificationsByTags(['user_visible_auto_notification']);
} finally { } finally {
res(); res();
} }

View file

@ -4,7 +4,6 @@
*/ */
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { SwMessage, SwMessageOrderType } from '@/types'; import { SwMessage, SwMessageOrderType } from '@/types';
import { acct as getAcct } from '@/filters/user';
import { getAccountFromId } from '@/scripts/get-account-from-id'; import { getAccountFromId } from '@/scripts/get-account-from-id';
import { getUrlWithLoginId } from '@/scripts/login-id'; import { getUrlWithLoginId } from '@/scripts/login-id';
@ -17,13 +16,27 @@ export async function api<E extends keyof Misskey.Endpoints>(endpoint: E, userId
return cli.request(endpoint, options, account.token); return cli.request(endpoint, options, account.token);
} }
// mark-all-as-read送出を1秒間隔に制限する
const readBlockingStatus = new Map<string, boolean>();
export function sendMarkAllAsRead(userId: string): Promise<null | undefined | void> {
if (readBlockingStatus.get(userId)) return Promise.resolve();
readBlockingStatus.set(userId, true);
return new Promise(resolve => {
setTimeout(() => {
readBlockingStatus.set(userId, false);
api('notifications/mark-all-as-read', userId)
.then(resolve, resolve);
}, 1000);
});
}
// rendered acctからユーザーを開く // rendered acctからユーザーを開く
export function openUser(acct: string, loginId: string) { export function openUser(acct: string, loginId?: string) {
return openClient('push', `/@${acct}`, loginId, { acct }); return openClient('push', `/@${acct}`, loginId, { acct });
} }
// noteIdからートを開く // noteIdからートを開く
export function openNote(noteId: string, loginId: string) { export function openNote(noteId: string, loginId?: string) {
return openClient('push', `/notes/${noteId}`, loginId, { noteId }); return openClient('push', `/notes/${noteId}`, loginId, { noteId });
} }
@ -33,7 +46,7 @@ export function openAntenna(antennaId: string, loginId: string) {
} }
// post-formのオプションから投稿フォームを開く // post-formのオプションから投稿フォームを開く
export async function openPost(options: any, loginId: string) { export async function openPost(options: any, loginId?: string) {
// クエリを作成しておく // クエリを作成しておく
let url = '/share?'; let url = '/share?';
if (options.initialText) url += `text=${options.initialText}&`; if (options.initialText) url += `text=${options.initialText}&`;
@ -43,7 +56,7 @@ export async function openPost(options: any, loginId: string) {
return openClient('post', url, loginId, { options }); return openClient('post', url, loginId, { options });
} }
export async function openClient(order: SwMessageOrderType, url: string, loginId: string, query: any = {}) { export async function openClient(order: SwMessageOrderType, url: string, loginId?: string, query: any = {}) {
const client = await findClient(); const client = await findClient();
if (client) { if (client) {
@ -51,7 +64,7 @@ export async function openClient(order: SwMessageOrderType, url: string, loginId
return client; return client;
} }
return globalThis.clients.openWindow(getUrlWithLoginId(url, loginId)); return globalThis.clients.openWindow(loginId ? getUrlWithLoginId(url, loginId) : url);
} }
export async function findClient() { export async function findClient() {
@ -59,7 +72,7 @@ export async function findClient() {
type: 'window', type: 'window',
}); });
for (const c of clients) { for (const c of clients) {
if (!new URL(c.url).searchParams.has('zen')) return c; if (!(new URL(c.url)).searchParams.has('zen')) return c;
} }
return null; return null;
} }

View file

@ -1,9 +1,9 @@
import { createEmptyNotification, createNotification } from '@/scripts/create-notification'; import { createEmptyNotification, createNotification } from '@/scripts/create-notification';
import { swLang } from '@/scripts/lang'; import { swLang } from '@/scripts/lang';
import { api } from '@/scripts/operations';
import { PushNotificationDataMap } from '@/types'; import { PushNotificationDataMap } from '@/types';
import * as swos from '@/scripts/operations'; import * as swos from '@/scripts/operations';
import { acct as getAcct } from '@/filters/user'; import { acct as getAcct } from '@/filters/user';
import { get } from 'idb-keyval';
globalThis.addEventListener('install', ev => { globalThis.addEventListener('install', ev => {
//ev.waitUntil(globalThis.skipWaiting()); //ev.waitUntil(globalThis.skipWaiting());
@ -54,6 +54,10 @@ globalThis.addEventListener('push', ev => {
if ((new Date()).getTime() - data.dateTime > 1000 * 60 * 60 * 24) break; if ((new Date()).getTime() - data.dateTime > 1000 * 60 * 60 * 24) break;
return createNotification(data); return createNotification(data);
case 'readAllNotifications':
await globalThis.registration.getNotifications()
.then(notifications => notifications.forEach(n => n.close()));
break;
} }
await createEmptyNotification(); await createEmptyNotification();
@ -68,7 +72,7 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
} }
const { action, notification } = ev; const { action, notification } = ev;
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data; const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {};
const { userId: loginId } = data; const { userId: loginId } = data;
let client: WindowClient | null = null; let client: WindowClient | null = null;
@ -124,13 +128,29 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
break; break;
case 'unreadAntennaNote': case 'unreadAntennaNote':
client = await swos.openAntenna(data.body.antenna.id, loginId); client = await swos.openAntenna(data.body.antenna.id, loginId);
break;
default:
switch (action) {
case 'markAllAsRead':
await globalThis.registration.getNotifications()
.then(notifications => notifications.forEach(n => n.close()));
await get('accounts').then(accounts => {
return Promise.all(accounts.map(async account => {
await swos.sendMarkAllAsRead(account.id);
}));
});
break;
case 'settings':
client = await swos.openClient('push', '/settings/notifications', loginId);
break;
}
} }
if (client) { if (client) {
client.focus(); client.focus();
} }
if (data.type === 'notification') { if (data.type === 'notification') {
api('notifications/mark-all-as-read', data.userId); await swos.sendMarkAllAsRead(loginId);
} }
notification.close(); notification.close();
@ -140,9 +160,12 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => { globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => {
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data; const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data;
ev.waitUntil((async () => {
if (data.type === 'notification') { if (data.type === 'notification') {
api('notifications/mark-all-as-read', data.userId); await swos.sendMarkAllAsRead(data.userId);
} }
return;
})());
}); });
globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => { globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => {

View file

@ -17,6 +17,7 @@ type PushNotificationDataSourceMap = {
antenna: { id: string, name: string }; antenna: { id: string, name: string };
note: Misskey.entities.Note; note: Misskey.entities.Note;
}; };
readAllNotifications: undefined;
}; };
export type PushNotificationData<K extends keyof PushNotificationDataSourceMap> = { export type PushNotificationData<K extends keyof PushNotificationDataSourceMap> = {
@ -37,6 +38,7 @@ export type BadgeNames =
| 'at' | 'at'
| 'chart-arrows' | 'chart-arrows'
| 'circle-check' | 'circle-check'
| 'medal'
| 'messages' | 'messages'
| 'plus' | 'plus'
| 'quote' | 'quote'