Fix notification (#2654)
This commit is contained in:
parent
21ec16b50f
commit
09cb1bba71
@ -38,10 +38,8 @@ export async function readNotificationByQuery(
|
|||||||
|
|
||||||
function postReadAllNotifications(userId: User['id']) {
|
function postReadAllNotifications(userId: User['id']) {
|
||||||
publishMainStream(userId, 'readAllNotifications');
|
publishMainStream(userId, 'readAllNotifications');
|
||||||
return pushNotification(userId, 'readAllNotifications', undefined);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
|
function postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
|
||||||
publishMainStream(userId, 'readNotifications', notificationIds);
|
publishMainStream(userId, 'readNotifications', notificationIds);
|
||||||
return pushNotification(userId, 'readNotifications', { notificationIds });
|
|
||||||
}
|
}
|
||||||
|
@ -29,5 +29,4 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||||||
|
|
||||||
// 全ての通知を読みましたよというイベントを発行
|
// 全ての通知を読みましたよというイベントを発行
|
||||||
publishMainStream(user.id, 'readAllNotifications');
|
publishMainStream(user.id, 'readAllNotifications');
|
||||||
pushNotification(user.id, 'readAllNotifications', undefined);
|
|
||||||
});
|
});
|
||||||
|
@ -20,30 +20,14 @@ export const meta = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
oneOf: [
|
|
||||||
{
|
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
notificationId: { type: 'string', format: 'misskey:id' },
|
notificationId: { type: 'string', format: 'misskey:id' },
|
||||||
},
|
},
|
||||||
required: ['notificationId'],
|
required: ['notificationId'],
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
notificationIds: {
|
|
||||||
type: 'array',
|
|
||||||
items: { type: 'string', format: 'misskey:id' },
|
|
||||||
maxItems: 100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['notificationIds'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
if ('notificationId' in ps) return readNotification(user.id, [ps.notificationId]);
|
return readNotification(user.id, [ps.notificationId]);
|
||||||
return readNotification(user.id, ps.notificationIds);
|
|
||||||
});
|
});
|
||||||
|
@ -60,6 +60,8 @@ export async function pushNotification<T extends keyof pushNotificationsTypes>(u
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//console.log(`send: ${subscription.endpoint?.substring(0, 64)}`);
|
||||||
|
//console.log(`send: ${type} ${JSON.stringify(body)}`);
|
||||||
push.sendNotification(pushSubscription, JSON.stringify({
|
push.sendNotification(pushSubscription, JSON.stringify({
|
||||||
type,
|
type,
|
||||||
body: type === 'notification' ? truncateNotification(body as Packed<'Notification'>) : body,
|
body: type === 'notification' ? truncateNotification(body as Packed<'Notification'>) : body,
|
||||||
@ -67,9 +69,9 @@ export async function pushNotification<T extends keyof pushNotificationsTypes>(u
|
|||||||
}), {
|
}), {
|
||||||
proxy: config.proxy,
|
proxy: config.proxy,
|
||||||
}).catch((err: any) => {
|
}).catch((err: any) => {
|
||||||
//swLogger.info(err.statusCode);
|
//console.log(err.statusCode);
|
||||||
//swLogger.info(err.headers);
|
//console.log(err.headers);
|
||||||
//swLogger.info(err.body);
|
//console.log(err.body);
|
||||||
|
|
||||||
if (err.statusCode === 410) {
|
if (err.statusCode === 410) {
|
||||||
SwSubscriptions.delete({
|
SwSubscriptions.delete({
|
||||||
|
@ -21,7 +21,6 @@ export async function createNotification<K extends keyof pushNotificationDataMap
|
|||||||
return self.registration.showNotification(...n);
|
return self.registration.showNotification(...n);
|
||||||
} else {
|
} else {
|
||||||
console.error('Could not compose notification', data);
|
console.error('Could not compose notification', data);
|
||||||
return createEmptyNotification();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,18 +43,11 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
// users/showの型定義をswos.apiへ当てはめるのが困難なのでapiFetch.requestを直接使用
|
// users/showの型定義をswos.apiへ当てはめるのが困難なのでapiFetch.requestを直接使用
|
||||||
const account = await getAccountFromId(data.userId);
|
const account = await getAccountFromId(data.userId);
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token);
|
|
||||||
return [t('_notification.youWereFollowed'), {
|
return [t('_notification.youWereFollowed'), {
|
||||||
body: getUserName(data.body.user),
|
body: getUserName(data.body.user),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('user-plus'),
|
badge: iconUrl('user-plus'),
|
||||||
data,
|
data,
|
||||||
actions: userDetail.isFollowing ? [] : [
|
|
||||||
{
|
|
||||||
action: 'follow',
|
|
||||||
title: t('_notification._actions.followBack')
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'mention':
|
case 'mention':
|
||||||
@ -64,12 +56,6 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('at'),
|
badge: iconUrl('at'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
action: 'reply',
|
|
||||||
title: t('_notification._actions.reply')
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'reply':
|
case 'reply':
|
||||||
@ -78,12 +64,6 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('reply'),
|
badge: iconUrl('reply'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
action: 'reply',
|
|
||||||
title: t('_notification._actions.reply')
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'renote':
|
case 'renote':
|
||||||
@ -92,12 +72,6 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('retweet'),
|
badge: iconUrl('retweet'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
action: 'showUser',
|
|
||||||
title: getUserName(data.body.user)
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'quote':
|
case 'quote':
|
||||||
@ -106,18 +80,6 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('quote-right'),
|
badge: iconUrl('quote-right'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
action: 'reply',
|
|
||||||
title: t('_notification._actions.reply')
|
|
||||||
},
|
|
||||||
...((data.body.note.visibility === 'public' || data.body.note.visibility === 'home') ? [
|
|
||||||
{
|
|
||||||
action: 'renote',
|
|
||||||
title: t('_notification._actions.renote')
|
|
||||||
}
|
|
||||||
] : [])
|
|
||||||
],
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'reaction':
|
case 'reaction':
|
||||||
@ -160,12 +122,6 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge,
|
badge,
|
||||||
data,
|
data,
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
action: 'showUser',
|
|
||||||
title: getUserName(data.body.user)
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'pollVote':
|
case 'pollVote':
|
||||||
@ -189,16 +145,6 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('clock'),
|
badge: iconUrl('clock'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
action: 'accept',
|
|
||||||
title: t('accept')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: 'reject',
|
|
||||||
title: t('reject')
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'followRequestAccepted':
|
case 'followRequestAccepted':
|
||||||
@ -214,16 +160,6 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
body: data.body.invitation.group.name,
|
body: data.body.invitation.group.name,
|
||||||
badge: iconUrl('id-card-alt'),
|
badge: iconUrl('id-card-alt'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
action: 'accept',
|
|
||||||
title: t('accept')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: 'reject',
|
|
||||||
title: t('reject')
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'app':
|
case 'app':
|
||||||
@ -257,33 +193,3 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createEmptyNotification() {
|
|
||||||
return new Promise<void>(async res => {
|
|
||||||
if (!swLang.i18n) swLang.fetchLocale();
|
|
||||||
const i18n = await swLang.i18n as I18n<any>;
|
|
||||||
const { t } = i18n;
|
|
||||||
|
|
||||||
await self.registration.showNotification(
|
|
||||||
t('_notification.emptyPushNotificationMessage'),
|
|
||||||
{
|
|
||||||
silent: true,
|
|
||||||
badge: iconUrl('null'),
|
|
||||||
tag: 'read_notification',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
res();
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
for (const n of
|
|
||||||
[
|
|
||||||
...(await self.registration.getNotifications({ tag: 'user_visible_auto_notification' })),
|
|
||||||
...(await self.registration.getNotifications({ tag: 'read_notification' }))
|
|
||||||
]
|
|
||||||
) {
|
|
||||||
n.close();
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
declare var self: ServiceWorkerGlobalScope;
|
|
||||||
|
|
||||||
import { get } from 'idb-keyval';
|
|
||||||
import { pushNotificationDataMap } from '@/types';
|
|
||||||
import { api } from '@/scripts/operations';
|
|
||||||
|
|
||||||
type Accounts = {
|
|
||||||
[x: string]: {
|
|
||||||
queue: string[],
|
|
||||||
timeout: number | null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class SwNotificationReadManager {
|
|
||||||
private accounts: Accounts = {};
|
|
||||||
|
|
||||||
public async construct() {
|
|
||||||
const accounts = await get('accounts');
|
|
||||||
if (!accounts) Error('Accounts are not recorded');
|
|
||||||
|
|
||||||
this.accounts = accounts.reduce((acc, e) => {
|
|
||||||
acc[e.id] = {
|
|
||||||
queue: [],
|
|
||||||
timeout: null
|
|
||||||
};
|
|
||||||
return acc;
|
|
||||||
}, {} as Accounts);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// プッシュ通知の既読をサーバーに送信
|
|
||||||
public async read<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) {
|
|
||||||
if (data.type !== 'notification' || !(data.userId in this.accounts)) return;
|
|
||||||
|
|
||||||
const account = this.accounts[data.userId];
|
|
||||||
|
|
||||||
account.queue.push(data.body.id as string);
|
|
||||||
|
|
||||||
if (account.queue.length >= 20) {
|
|
||||||
if (account.timeout) clearTimeout(account.timeout);
|
|
||||||
const notificationIds = account.queue;
|
|
||||||
account.queue = [];
|
|
||||||
await api('notifications/read', data.userId, { notificationIds });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最後の呼び出しから200ms待ってまとめて処理する
|
|
||||||
if (account.timeout) clearTimeout(account.timeout);
|
|
||||||
account.timeout = setTimeout(() => {
|
|
||||||
account.timeout = null;
|
|
||||||
|
|
||||||
const notificationIds = account.queue;
|
|
||||||
account.queue = [];
|
|
||||||
api('notifications/read', data.userId, { notificationIds });
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const swNotificationRead = (new SwNotificationReadManager()).construct();
|
|
@ -1,11 +1,8 @@
|
|||||||
declare var self: ServiceWorkerGlobalScope;
|
declare var self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
import { createEmptyNotification, createNotification } from '@/scripts/create-notification';
|
import { createNotification } from '@/scripts/create-notification';
|
||||||
import { swLang } from '@/scripts/lang';
|
import { swLang } from '@/scripts/lang';
|
||||||
import { swNotificationRead } from '@/scripts/notification-read';
|
|
||||||
import { pushNotificationDataMap } from '@/types';
|
import { pushNotificationDataMap } from '@/types';
|
||||||
import * as swos from '@/scripts/operations';
|
|
||||||
import { acct as getAcct } from '@/filters/user';
|
|
||||||
|
|
||||||
self.addEventListener('install', ev => {
|
self.addEventListener('install', ev => {
|
||||||
ev.waitUntil(self.skipWaiting());
|
ev.waitUntil(self.skipWaiting());
|
||||||
@ -39,139 +36,33 @@ self.addEventListener('push', ev => {
|
|||||||
const data: pushNotificationDataMap[K] = ev.data?.json();
|
const data: pushNotificationDataMap[K] = ev.data?.json();
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
// case 'driveFileCreated':
|
|
||||||
case 'notification':
|
case 'notification':
|
||||||
case 'unreadMessagingMessage':
|
case 'unreadMessagingMessage':
|
||||||
// クライアントがあったらストリームに接続しているということなので通知しない
|
// クライアントがあったらストリームに接続しているということなので通知しない
|
||||||
if (clients.length != 0) return;
|
if (clients.length != 0) return;
|
||||||
return createNotification(data);
|
return createNotification(data);
|
||||||
case 'readAllNotifications':
|
|
||||||
for (const n of await self.registration.getNotifications()) {
|
|
||||||
if (n?.data?.type === 'notification') n.close();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'readAllMessagingMessages':
|
|
||||||
for (const n of await self.registration.getNotifications()) {
|
|
||||||
if (n?.data?.type === 'unreadMessagingMessage') n.close();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'readNotifications':
|
|
||||||
for (const n of await self.registration.getNotifications()) {
|
|
||||||
if (data.body?.notificationIds?.includes(n.data.body.id)) {
|
|
||||||
n.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'readAllMessagingMessagesOfARoom':
|
|
||||||
for (const n of await self.registration.getNotifications()) {
|
|
||||||
if (n.data.type === 'unreadMessagingMessage'
|
|
||||||
&& ('userId' in data.body
|
|
||||||
? data.body.userId === n.data.body.userId
|
|
||||||
: data.body.groupId === n.data.body.groupId)
|
|
||||||
) {
|
|
||||||
n.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return createEmptyNotification();
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('notificationclick', <K extends keyof pushNotificationDataMap>(ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => {
|
self.addEventListener('notificationclick', <K extends keyof pushNotificationDataMap>(ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => {
|
||||||
ev.waitUntil((async () => {
|
ev.notification.close();
|
||||||
if (_DEV_) {
|
|
||||||
console.log('notificationclick', ev.action, ev.notification.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { action, notification } = ev;
|
|
||||||
const data: pushNotificationDataMap[K] = notification.data;
|
|
||||||
const { userId: id } = data;
|
|
||||||
let client: WindowClient | null = null;
|
|
||||||
|
|
||||||
switch (data.type) {
|
|
||||||
case 'notification':
|
|
||||||
switch (action) {
|
|
||||||
case 'follow':
|
|
||||||
if ('userId' in data.body) await swos.api('following/create', id, { userId: data.body.userId });
|
|
||||||
break;
|
|
||||||
case 'showUser':
|
|
||||||
if ('user' in data.body) client = await swos.openUser(getAcct(data.body.user), id);
|
|
||||||
break;
|
|
||||||
case 'reply':
|
|
||||||
if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, id);
|
|
||||||
break;
|
|
||||||
case 'renote':
|
|
||||||
if ('note' in data.body) await swos.api('notes/create', id, { renoteId: data.body.note.id });
|
|
||||||
break;
|
|
||||||
case 'accept':
|
|
||||||
switch (data.body.type) {
|
|
||||||
case 'receiveFollowRequest':
|
|
||||||
await swos.api('following/requests/accept', id, { userId: data.body.userId });
|
|
||||||
break;
|
|
||||||
case 'groupInvited':
|
|
||||||
await swos.api('users/groups/invitations/accept', id, { invitationId: data.body.invitation.id });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'reject':
|
|
||||||
switch (data.body.type) {
|
|
||||||
case 'receiveFollowRequest':
|
|
||||||
await swos.api('following/requests/reject', id, { userId: data.body.userId });
|
|
||||||
break;
|
|
||||||
case 'groupInvited':
|
|
||||||
await swos.api('users/groups/invitations/reject', id, { invitationId: data.body.invitation.id });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'showFollowRequests':
|
|
||||||
client = await swos.openClient('push', '/my/follow-requests', id);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
switch (data.body.type) {
|
|
||||||
case 'receiveFollowRequest':
|
|
||||||
client = await swos.openClient('push', '/my/follow-requests', id);
|
|
||||||
break;
|
|
||||||
case 'groupInvited':
|
|
||||||
client = await swos.openClient('push', '/my/groups', id);
|
|
||||||
break;
|
|
||||||
case 'reaction':
|
|
||||||
client = await swos.openNote(data.body.note.id, id);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if ('note' in data.body) {
|
|
||||||
client = await swos.openNote(data.body.note.id, id);
|
|
||||||
} else if ('user' in data.body) {
|
|
||||||
client = await swos.openUser(getAcct(data.body.user), id);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'unreadMessagingMessage':
|
|
||||||
client = await swos.openChat(data.body, id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client) {
|
|
||||||
client.focus();
|
|
||||||
}
|
|
||||||
if (data.type === 'notification') {
|
|
||||||
swNotificationRead.then(that => that.read(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
notification.close();
|
|
||||||
|
|
||||||
})());
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('notificationclose', <K extends keyof pushNotificationDataMap>(ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => {
|
ev.waitUntil(self.clients.matchAll({
|
||||||
const data: pushNotificationDataMap[K] = ev.notification.data;
|
type: "window"
|
||||||
|
}).then(clientList => {
|
||||||
|
for (let i = 0; i < clientList.length; i++) {
|
||||||
|
const client = clientList[i];
|
||||||
|
if (client.url == '/' && 'focus' in client) {
|
||||||
|
return client.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (self.clients.openWindow) {
|
||||||
|
return self.clients.openWindow('/');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
|
||||||
if (data.type === 'notification') {
|
|
||||||
swNotificationRead.then(that => that.read(data));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => {
|
self.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user