diff --git a/src/client/sw/create-notification.ts b/src/client/sw/create-notification.ts index 23eb2b9817..77a7729621 100644 --- a/src/client/sw/create-notification.ts +++ b/src/client/sw/create-notification.ts @@ -7,21 +7,21 @@ import { getNoteSummary } from '@/misc/get-note-summary'; import getUserName from '@/misc/get-user-name'; import { swLang } from '@client/sw/lang'; import { I18n } from '@/misc/i18n'; -import { pushNotificationData } from '@/types'; +import { pushNotificationDataMap } from '@client/sw/types'; +import { apiFetch } from './operations'; +import { getAccountFromId } from '@client/scripts/get-account-from-id'; -export async function createNotification(data: pushNotificationData) { +export async function createNotification(data: pushNotificationDataMap[K]) { const n = await composeNotification(data); if (n) await self.registration.showNotification(...n); else await createEmptyNotification(); } -async function composeNotification(data: pushNotificationData): Promise<[string, NotificationOptions] | null | undefined> { +async function composeNotification(data: pushNotificationDataMap[K]): Promise<[string, NotificationOptions] | null | undefined> { if (!swLang.i18n) swLang.fetchLocale(); const i18n = await swLang.i18n as I18n; const { t } = i18n; - const { body } = data; - switch (data.type) { /* case 'driveFileCreated': // TODO (Server Side) @@ -32,13 +32,17 @@ async function composeNotification(data: pushNotificationData): Promise<[string, }]; */ case 'notification': - switch (body.type) { + switch (data.body.type) { case 'follow': + // users/showの型定義をswos.apiへ当てはめるのが困難なのでapiFetch.requestを直接使用 + const account = await getAccountFromId(data.userId); + if (!account) return null; + const userDetail = apiFetch.request('users/show', { userId: data.body.userId }, account?.token); return [t('_notification.youWereFollowed'), { - body: getUserName(body.user), - icon: body.user.avatarUrl, + body: getUserName(data.body.user), + icon: data.body.user.avatarUrl, data, - actions: [ + actions: userDetail.isFollowing ? [] : [ { action: 'follow', title: t('_notification._actions.followBack') @@ -47,9 +51,9 @@ async function composeNotification(data: pushNotificationData): Promise<[string, }]; case 'mention': - return [t('_notification.youGotMention', { name: getUserName(body.user) }), { - body: getNoteSummary(body.note, i18n.locale), - icon: body.user.avatarUrl, + return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), { + body: getNoteSummary(data.body.note, i18n.locale), + icon: data.body.user.avatarUrl, data, actions: [ { @@ -60,9 +64,9 @@ async function composeNotification(data: pushNotificationData): Promise<[string, }]; case 'reply': - return [t('_notification.youGotReply', { name: getUserName(body.user) }), { - body: getNoteSummary(body.note, i18n.locale), - icon: body.user.avatarUrl, + return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), { + body: getNoteSummary(data.body.note, i18n.locale), + icon: data.body.user.avatarUrl, data, actions: [ { @@ -73,22 +77,22 @@ async function composeNotification(data: pushNotificationData): Promise<[string, }]; case 'renote': - return [t('_notification.youRenoted', { name: getUserName(body.user) }), { - body: getNoteSummary(body.note.renote, i18n.locale), - icon: body.user.avatarUrl, + return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), { + body: getNoteSummary(data.body.note.renote, i18n.locale), + icon: data.body.user.avatarUrl, data, actions: [ { action: 'showUser', - title: getUserName(body.user) + title: getUserName(data.body.user) } ], }]; case 'quote': - return [t('_notification.youGotQuote', { name: getUserName(body.user) }), { - body: getNoteSummary(body.note, i18n.locale), - icon: body.user.avatarUrl, + return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), { + body: getNoteSummary(data.body.note, i18n.locale), + icon: data.body.user.avatarUrl, data, actions: [ { @@ -103,29 +107,29 @@ async function composeNotification(data: pushNotificationData): Promise<[string, }]; case 'reaction': - return [`${body.reaction} ${getUserName(body.user)}`, { - body: getNoteSummary(body.note, i18n.locale), - icon: body.user.avatarUrl, + return [`${data.body.reaction} ${getUserName(data.body.user)}`, { + body: getNoteSummary(data.body.note, i18n.locale), + icon: data.body.user.avatarUrl, data, actions: [ { action: 'showUser', - title: getUserName(body.user) + title: getUserName(data.body.user) } ], }]; case 'pollVote': - return [t('_notification.youGotPoll', { name: getUserName(body.user) }), { - body: getNoteSummary(body.note, i18n.locale), - icon: body.user.avatarUrl, + return [t('_notification.youGotPoll', { name: getUserName(data.body.user) }), { + body: getNoteSummary(data.body.note, i18n.locale), + icon: data.body.user.avatarUrl, data, }]; case 'receiveFollowRequest': return [t('_notification.youReceivedFollowRequest'), { - body: getUserName(body.user), - icon: body.user.avatarUrl, + body: getUserName(data.body.user), + icon: data.body.user.avatarUrl, data, actions: [ { @@ -141,14 +145,14 @@ async function composeNotification(data: pushNotificationData): Promise<[string, case 'followRequestAccepted': return [t('_notification.yourFollowRequestAccepted'), { - body: getUserName(body.user), - icon: body.user.avatarUrl, + body: getUserName(data.body.user), + icon: data.body.user.avatarUrl, data, }]; case 'groupInvited': - return [t('_notification.youWereInvitedToGroup', { userName: getUserName(body.user) }), { - body: body.invitation.group.name, + return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.group) }), { + body: data.body.invitation.group.name, data, actions: [ { @@ -163,27 +167,27 @@ async function composeNotification(data: pushNotificationData): Promise<[string, }]; case 'app': - return [body.header, { - body: body.body, - icon: body.icon, + return [data.body.header || data.body.body, { + body: data.body.header && data.body.body, + icon: data.body.icon, data }]; - + default: return null; } case 'unreadMessagingMessage': - if (body.groupId === null) { - return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(body.user) }), { - icon: body.user.avatarUrl, - tag: `messaging:user:${body.userId}`, + if (data.body.groupId === null) { + return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), { + icon: data.body.user.avatarUrl, + tag: `messaging:user:${data.body.userId}`, data, renotify: true, }]; } - return [t('_notification.youGotMessagingMessageFromGroup', { name: body.group.name }), { - icon: body.user.avatarUrl, - tag: `messaging:group:${body.groupId}`, + return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), { + icon: data.body.user.avatarUrl, + tag: `messaging:group:${data.body.groupId}`, data, renotify: true, }]; diff --git a/src/client/sw/notification-read.ts b/src/client/sw/notification-read.ts index b0b73aace8..bc76a1ac38 100644 --- a/src/client/sw/notification-read.ts +++ b/src/client/sw/notification-read.ts @@ -1,7 +1,7 @@ declare var self: ServiceWorkerGlobalScope; import { get } from 'idb-keyval'; -import { pushNotificationData } from '../../types'; +import { pushNotificationDataMap } from '@client/sw/types'; import { api } from './operations'; type Accounts = { @@ -30,7 +30,7 @@ class SwNotificationReadManager { } // プッシュ通知の既読をサーバーに送信 - public async read(data: pushNotificationData) { + public async read(data: pushNotificationDataMap[K]) { if (data.type !== 'notification' || !(data.userId in this.accounts)) return; const account = this.accounts[data.userId]; diff --git a/src/client/sw/operations.ts b/src/client/sw/operations.ts index 3dc71bd551..b0f4f50f53 100644 --- a/src/client/sw/operations.ts +++ b/src/client/sw/operations.ts @@ -4,29 +4,19 @@ */ declare var self: ServiceWorkerGlobalScope; +import * as Misskey from 'misskey-js'; import { SwMessage, swMessageOrderType } from './types'; import { getAcct } from '@/misc/acct'; import { getAccountFromId } from '@client/scripts/get-account-from-id'; import { appendLoginId } from '@client/scripts/login-id'; -export async function api(endpoint: string, userId: string, options: any = {}) { +export const apiFetch = new Misskey.api.APIClient({ origin, fetch }); + +export async function api(endpoint: E, userId: string, options?: Misskey.Endpoints[E]['req']) { const account = await getAccountFromId(userId); if (!account) return; - return fetch(`${origin}/api/${endpoint}`, { - method: 'POST', - body: JSON.stringify({ - i: account.token, - ...options - }), - credentials: 'omit', - cache: 'no-cache', - }).then(async res => { - if (!res.ok) Error(`Error while fetching: ${await res.text()}`); - - if (res.status === 200) return res.json(); - return; - }); + return apiFetch.request(endpoint, options, account.token); } // rendered acctからユーザーを開く @@ -59,14 +49,7 @@ export async function openPost(options: any, loginId: string) { } export async function openClient(order: swMessageOrderType, url: string, loginId: string, query: any = {}) { - const client = await self.clients.matchAll({ - type: 'window' - }).then(clients => { - for (const c of clients) { - if (c.url.indexOf('?zen') < 0) return c; - } - return null; - }); + const client = await findClient(); if (client) { client.postMessage({ type: 'order', ...query, order, loginId, url } as SwMessage); @@ -75,3 +58,13 @@ export async function openClient(order: swMessageOrderType, url: string, loginId return self.clients.openWindow(appendLoginId(url, loginId)); } + +export async function findClient() { + const clients = await self.clients.matchAll({ + type: 'window' + }); + for (const c of clients) { + if (c.url.indexOf('?zen') < 0) return c; + } + return null; +} diff --git a/src/client/sw/sw.ts b/src/client/sw/sw.ts index fac8750975..7a0ba30d91 100644 --- a/src/client/sw/sw.ts +++ b/src/client/sw/sw.ts @@ -6,8 +6,8 @@ declare var self: ServiceWorkerGlobalScope; import { createEmptyNotification, createNotification } from '@client/sw/create-notification'; import { swLang } from '@client/sw/lang'; import { swNotificationRead } from '@client/sw/notification-read'; -import { pushNotificationData } from '@/types'; -import * as ope from './operations'; +import { pushNotificationDataMap } from '@client/sw/types'; +import * as swos from './operations'; import { getAcct } from '@/misc/acct'; //#region Lifecycle: Install @@ -45,11 +45,11 @@ self.addEventListener('push', ev => { ev.waitUntil(self.clients.matchAll({ includeUncontrolled: true, type: 'window' - }).then(async clients => { + }).then(async (clients: readonly WindowClient[]) => { // // クライアントがあったらストリームに接続しているということなので通知しない // if (clients.length != 0) return; - const data: pushNotificationData = ev.data?.json(); + const data: pushNotificationDataMap[K] = ev.data?.json(); switch (data.type) { // case 'driveFileCreated': @@ -103,7 +103,7 @@ self.addEventListener('push', ev => { //#endregion //#region Notification -self.addEventListener('notificationclick', ev => { +self.addEventListener('notificationclick', (ev: NotificationEvent) => { ev.waitUntil((async () => { if (_DEV_) { @@ -111,85 +111,90 @@ self.addEventListener('notificationclick', ev => { } const { action, notification } = ev; - const data: pushNotificationData = notification.data; - const { type, userId: id, body } = data; + const data: pushNotificationDataMap[K] = notification.data; + const { userId: id } = data; let client: WindowClient | null = null; - let close = true; - switch (action) { - case 'follow': - client = await ope.api('following/create', id, { userId: body.userId }); - break; - case 'showUser': - client = await ope.openUser(getAcct(body.user), id); - if (body.type !== 'renote') close = false; - break; - case 'reply': - client = await ope.openPost({ reply: body.note }, id); - break; - case 'renote': - await ope.api('notes/create', id, { renoteId: body.note.id }); - break; - case 'accept': - if (body.type === 'receiveFollowRequest') { - await ope.api('following/requests/accept', id, { userId: body.userId }); - } else if (body.type === 'groupInvited') { - await ope.api('users/groups/invitations/accept', id, { invitationId: body.invitation.id }); - } - break; - case 'reject': - if (body.type === 'receiveFollowRequest') { - await ope.api('following/requests/reject', id, { userId: body.userId }); - } else if (body.type === 'groupInvited') { - await ope.api('users/groups/invitations/reject', id, { invitationId: body.invitation.id }); - } - break; - case 'showFollowRequests': - client = await ope.openClient('push', '/my/follow-requests', id); - break; - default: - if (type === 'unreadMessagingMessage') { - client = await ope.openChat(body, id); - break; - } - - switch (body.type) { - case 'receiveFollowRequest': - client = await ope.openClient('push', '/my/follow-requests', id); + switch (data.type) { + case 'notification': + switch (action) { + case 'follow': + await swos.api('following/create', id, { userId: data.body.userId }); break; - case 'groupInvited': - client = await ope.openClient('push', '/my/groups', id); + case 'showUser': + if ('user' in data.body) client = await swos.openUser(getAcct(data.body.user), id); break; - case 'reaction': - client = await ope.openNote(body.note.id, id); + 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: - if ('note' in body) { - client = await ope.openNote(body.note.id, id); - break; - } - if ('user' in body) { - client = await ope.openUser(getAcct(body.data.user), id); - break; + 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); + break; + } + 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 (type === 'notification') { + if (data.type === 'notification') { swNotificationRead.then(that => that.read(data)); } - if (close) { - notification.close(); - } + + notification.close(); })()); }); -self.addEventListener('notificationclose', ev => { - const data: pushNotificationData = ev.notification.data; +self.addEventListener('notificationclose', (ev: NotificationEvent) => { + const data: pushNotificationDataMap[K] = ev.notification.data; if (data.type === 'notification') { swNotificationRead.then(that => that.read(data)); diff --git a/src/client/sw/types.ts b/src/client/sw/types.ts index 14bf7af96a..6aa3726eac 100644 --- a/src/client/sw/types.ts +++ b/src/client/sw/types.ts @@ -1,3 +1,5 @@ +import * as Misskey from 'misskey-js'; + export type swMessageOrderType = 'post' | 'push'; export type SwMessage = { @@ -7,3 +9,23 @@ export type SwMessage = { url: string; [x: string]: any; }; + +// Defined also @/services/push-notification.ts#L7-L14 +type pushNotificationDataSourceMap = { + notification: Misskey.entities.Notification; + unreadMessagingMessage: Misskey.entities.MessagingMessage; + readNotifications: { notificationIds: string[] }; + readAllNotifications: undefined; + readAllMessagingMessages: undefined; + readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string }; +}; + +export type pushNotificationData = { + type: K; + body: pushNotificationDataSourceMap[K]; + userId: string; +}; + +export type pushNotificationDataMap = { + [K in keyof pushNotificationDataSourceMap]: pushNotificationData; +}; diff --git a/src/misc/get-user-name.ts b/src/misc/get-user-name.ts index 3545e986e8..d499ea0203 100644 --- a/src/misc/get-user-name.ts +++ b/src/misc/get-user-name.ts @@ -1,5 +1,3 @@ -import { User } from '@/models/entities/user'; - -export default function(user: User): string { +export default function(user: { name?: string | null, username: string }): string { return user.name || user.username; } diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts index de58a839df..b4e1c4fd8a 100644 --- a/src/services/push-notification.ts +++ b/src/services/push-notification.ts @@ -3,8 +3,8 @@ import config from '@/config/index'; import { SwSubscriptions } from '@/models/index'; import { fetchMeta } from '@/misc/fetch-meta'; import { Packed } from '@/misc/schema'; -import { pushNotificationData } from '@/types'; +// Defined also @client/sw/types.ts#L14-L21 type pushNotificationsTypes = { 'notification': Packed<'Notification'>; 'unreadMessagingMessage': Packed<'MessagingMessage'>; @@ -40,7 +40,7 @@ export async function pushNotification(u push.sendNotification(pushSubscription, JSON.stringify({ type, body, userId - } as pushNotificationData), { + }), { proxy: config.proxy }).catch((err: any) => { //swLogger.info(err.statusCode); diff --git a/src/types.ts b/src/types.ts index c6a40510f2..d8eb442810 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,20 +3,3 @@ export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; - -export type pushNotificationData = { - type: 'notification' | 'unreadMessagingMessage' | 'readNotifications' | 'readAllMessagingMessagesOfARoom' | 'readAllNotifications' | 'readAllMessagingMessages'; - body: { - [x: string]: any; - id?: string; - type?: typeof notificationTypes[number]; - notificationIds?: string[]; - user?: any; - userId?: string | null; - note?: any; - choice?: number; - reaction?: string; - invitation?: any; - }; - userId: string; -};