forked from mirror/misskey
sw: なんかもうめっちゃ変えた (#10570)
* sw: なんかいろいろ * remove debug code * never renotify * update changelog.md
This commit is contained in:
parent
f6dc100748
commit
3a90bcc03c
@ -28,6 +28,13 @@
|
|||||||
- アンテナのノート、チャンネルのノート、通知が正常に作成できないことがある問題を修正
|
- アンテナのノート、チャンネルのノート、通知が正常に作成できないことがある問題を修正
|
||||||
- ストリーミングのLTLチャンネルでサーバー側にエラーログが出るのを修正
|
- ストリーミングのLTLチャンネルでサーバー側にエラーログが出るのを修正
|
||||||
|
|
||||||
|
## Service Worker
|
||||||
|
- 「通知が既読になったらプッシュ通知を削除する」を復活
|
||||||
|
* 「プッシュ通知が更新されました」の挙動を変えた(ホストとバージョンを表示するようにし、一定時間後の削除は行わないように)
|
||||||
|
- プッシュ通知が実績を解除 (achievementEarned) に対応
|
||||||
|
- プッシュ通知のアクションから既存のクライアントの投稿フォームを開くことになった際の挙動を修正
|
||||||
|
- たくさんのプッシュ通知を閉じた際、その通知の数だけnotifications/mark-all-as-readを叩くのをやめるように
|
||||||
|
|
||||||
## 13.11.1
|
## 13.11.1
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
@ -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: "元に戻す"
|
||||||
|
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
|
@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
|
||||||
|
@ -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: {
|
||||||
|
@ -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' });
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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']) => {
|
||||||
|
@ -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'
|
||||||
|
Loading…
Reference in New Issue
Block a user