sw: なんかもうめっちゃ変えた (#10570)
* sw: なんかいろいろ * remove debug code * never renotify * update changelog.md
This commit is contained in:
parent
f6dc100748
commit
3a90bcc03c
10 changed files with 106 additions and 31 deletions
|
@ -28,6 +28,13 @@
|
|||
- アンテナのノート、チャンネルのノート、通知が正常に作成できないことがある問題を修正
|
||||
- ストリーミングのLTLチャンネルでサーバー側にエラーログが出るのを修正
|
||||
|
||||
## Service Worker
|
||||
- 「通知が既読になったらプッシュ通知を削除する」を復活
|
||||
* 「プッシュ通知が更新されました」の挙動を変えた(ホストとバージョンを表示するようにし、一定時間後の削除は行わないように)
|
||||
- プッシュ通知が実績を解除 (achievementEarned) に対応
|
||||
- プッシュ通知のアクションから既存のクライアントの投稿フォームを開くことになった際の挙動を修正
|
||||
- たくさんのプッシュ通知を閉じた際、その通知の数だけnotifications/mark-all-as-readを叩くのをやめるように
|
||||
|
||||
## 13.11.1
|
||||
|
||||
### General
|
||||
|
|
|
@ -20,6 +20,7 @@ noNotes: "ノートはありません"
|
|||
noNotifications: "通知はありません"
|
||||
instance: "サーバー"
|
||||
settings: "設定"
|
||||
notificationSettings: "通知の設定"
|
||||
basicSettings: "基本設定"
|
||||
otherSettings: "その他の設定"
|
||||
openInWindow: "ウィンドウで開く"
|
||||
|
@ -917,8 +918,8 @@ subscribePushNotification: "プッシュ通知を有効化"
|
|||
unsubscribePushNotification: "プッシュ通知を停止する"
|
||||
pushNotificationAlreadySubscribed: "プッシュ通知は有効です"
|
||||
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に非対応"
|
||||
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
|
||||
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
|
||||
sendPushNotificationReadMessage: "通知が既読になったらプッシュ通知を削除する"
|
||||
sendPushNotificationReadMessageCaption: "端末の電池消費量が増加する可能性があります。"
|
||||
windowMaximize: "最大化"
|
||||
windowMinimize: "最小化"
|
||||
windowRestore: "元に戻す"
|
||||
|
|
BIN
packages/backend/assets/tabler-badges/medal.png
Normal file
BIN
packages/backend/assets/tabler-badges/medal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
|
@ -66,6 +66,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
private postReadAllNotifications(userId: User['id']) {
|
||||
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
|
||||
this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -15,6 +15,7 @@ type PushNotificationsTypes = {
|
|||
antenna: { id: string, name: string };
|
||||
note: Packed<'Note'>;
|
||||
};
|
||||
'readAllNotifications': undefined;
|
||||
};
|
||||
|
||||
// Reduce length because push message servers have character limits
|
||||
|
@ -68,6 +69,10 @@ export class PushNotificationService {
|
|||
});
|
||||
|
||||
for (const subscription of subscriptions) {
|
||||
if ([
|
||||
'readAllNotifications',
|
||||
].includes(type) && !subscription.sendReadMessage) continue;
|
||||
|
||||
const pushSubscription = {
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import { post } from '@/os';
|
||||
import { api, post } from '@/os';
|
||||
import { $i, login } from '@/account';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
import { mainRouter } from '@/router';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
|
||||
export function swInject() {
|
||||
navigator.serviceWorker.addEventListener('message', ev => {
|
||||
navigator.serviceWorker.addEventListener('message', async ev => {
|
||||
if (_DEV_) {
|
||||
console.log('sw msg', ev.data);
|
||||
}
|
||||
|
||||
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 => {
|
||||
if (!account) return;
|
||||
return login(account.token, ev.data.url);
|
||||
|
@ -19,8 +20,18 @@ export function swInject() {
|
|||
}
|
||||
|
||||
switch (ev.data.order) {
|
||||
case 'post':
|
||||
return post(ev.data.options);
|
||||
case 'post': {
|
||||
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':
|
||||
if (mainRouter.currentRoute.value.path === ev.data.url) {
|
||||
return window.scroll({ top: 0, behavior: 'smooth' });
|
||||
|
|
|
@ -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/
|
||||
* 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/
|
||||
* 4. Add 'icon-name' to badgeNames
|
||||
* 4. Add 'icon-name' to BadgeNames
|
||||
* 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':
|
||||
return [t('_notification.youReceivedFollowRequest'), {
|
||||
body: getUserName(data.body.user),
|
||||
|
@ -202,6 +194,14 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
|||
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':
|
||||
return [data.body.header ?? data.body.body, {
|
||||
body: data.body.header ? data.body.body : '',
|
||||
|
@ -233,17 +233,29 @@ export async function createEmptyNotification() {
|
|||
const { t } = i18n;
|
||||
|
||||
await globalThis.registration.showNotification(
|
||||
t('_notification.emptyPushNotificationMessage'),
|
||||
(new URL(origin)).host,
|
||||
{
|
||||
body: `Misskey v${_VERSION_}`,
|
||||
silent: true,
|
||||
badge: iconUrl('null'),
|
||||
tag: 'read_notification',
|
||||
actions: [
|
||||
{
|
||||
action: 'markAllAsRead',
|
||||
title: t('markAllAsRead'),
|
||||
},
|
||||
{
|
||||
action: 'settings',
|
||||
title: t('notificationSettings'),
|
||||
},
|
||||
],
|
||||
data: {},
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await closeNotificationsByTags(['user_visible_auto_notification', 'read_notification']);
|
||||
await closeNotificationsByTags(['user_visible_auto_notification']);
|
||||
} finally {
|
||||
res();
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
*/
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { SwMessage, SwMessageOrderType } from '@/types';
|
||||
import { acct as getAcct } from '@/filters/user';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-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);
|
||||
}
|
||||
|
||||
// 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からユーザーを開く
|
||||
export function openUser(acct: string, loginId: string) {
|
||||
export function openUser(acct: string, loginId?: string) {
|
||||
return openClient('push', `/@${acct}`, loginId, { acct });
|
||||
}
|
||||
|
||||
// noteIdからノートを開く
|
||||
export function openNote(noteId: string, loginId: string) {
|
||||
export function openNote(noteId: string, loginId?: string) {
|
||||
return openClient('push', `/notes/${noteId}`, loginId, { noteId });
|
||||
}
|
||||
|
||||
|
@ -33,7 +46,7 @@ export function openAntenna(antennaId: string, loginId: string) {
|
|||
}
|
||||
|
||||
// post-formのオプションから投稿フォームを開く
|
||||
export async function openPost(options: any, loginId: string) {
|
||||
export async function openPost(options: any, loginId?: string) {
|
||||
// クエリを作成しておく
|
||||
let url = '/share?';
|
||||
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 });
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (client) {
|
||||
|
@ -51,7 +64,7 @@ export async function openClient(order: SwMessageOrderType, url: string, loginId
|
|||
return client;
|
||||
}
|
||||
|
||||
return globalThis.clients.openWindow(getUrlWithLoginId(url, loginId));
|
||||
return globalThis.clients.openWindow(loginId ? getUrlWithLoginId(url, loginId) : url);
|
||||
}
|
||||
|
||||
export async function findClient() {
|
||||
|
@ -59,7 +72,7 @@ export async function findClient() {
|
|||
type: 'window',
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { createEmptyNotification, createNotification } from '@/scripts/create-notification';
|
||||
import { swLang } from '@/scripts/lang';
|
||||
import { api } from '@/scripts/operations';
|
||||
import { PushNotificationDataMap } from '@/types';
|
||||
import * as swos from '@/scripts/operations';
|
||||
import { acct as getAcct } from '@/filters/user';
|
||||
import { get } from 'idb-keyval';
|
||||
|
||||
globalThis.addEventListener('install', ev => {
|
||||
//ev.waitUntil(globalThis.skipWaiting());
|
||||
|
@ -54,6 +54,10 @@ globalThis.addEventListener('push', ev => {
|
|||
if ((new Date()).getTime() - data.dateTime > 1000 * 60 * 60 * 24) break;
|
||||
|
||||
return createNotification(data);
|
||||
case 'readAllNotifications':
|
||||
await globalThis.registration.getNotifications()
|
||||
.then(notifications => notifications.forEach(n => n.close()));
|
||||
break;
|
||||
}
|
||||
|
||||
await createEmptyNotification();
|
||||
|
@ -68,7 +72,7 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
|||
}
|
||||
|
||||
const { action, notification } = ev;
|
||||
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data;
|
||||
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {};
|
||||
const { userId: loginId } = data;
|
||||
let client: WindowClient | null = null;
|
||||
|
||||
|
@ -124,13 +128,29 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
|||
break;
|
||||
case 'unreadAntennaNote':
|
||||
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) {
|
||||
client.focus();
|
||||
}
|
||||
if (data.type === 'notification') {
|
||||
api('notifications/mark-all-as-read', data.userId);
|
||||
await swos.sendMarkAllAsRead(loginId);
|
||||
}
|
||||
|
||||
notification.close();
|
||||
|
@ -140,9 +160,12 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
|||
globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => {
|
||||
const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data;
|
||||
|
||||
if (data.type === 'notification') {
|
||||
api('notifications/mark-all-as-read', data.userId);
|
||||
}
|
||||
ev.waitUntil((async () => {
|
||||
if (data.type === 'notification') {
|
||||
await swos.sendMarkAllAsRead(data.userId);
|
||||
}
|
||||
return;
|
||||
})());
|
||||
});
|
||||
|
||||
globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => {
|
||||
|
|
|
@ -17,6 +17,7 @@ type PushNotificationDataSourceMap = {
|
|||
antenna: { id: string, name: string };
|
||||
note: Misskey.entities.Note;
|
||||
};
|
||||
readAllNotifications: undefined;
|
||||
};
|
||||
|
||||
export type PushNotificationData<K extends keyof PushNotificationDataSourceMap> = {
|
||||
|
@ -37,6 +38,7 @@ export type BadgeNames =
|
|||
| 'at'
|
||||
| 'chart-arrows'
|
||||
| 'circle-check'
|
||||
| 'medal'
|
||||
| 'messages'
|
||||
| 'plus'
|
||||
| 'quote'
|
||||
|
|
Loading…
Reference in a new issue