diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 394577f378..bb1c41d866 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1567,7 +1567,7 @@ _notification: youWereFollowed: "フォローされました" youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" - youWereInvitedToGroup: "グループに招待されました" + youWereInvitedToGroup: "{userName}があなたをグループに招待しました" _types: all: "すべて" @@ -1583,6 +1583,11 @@ _notification: groupInvited: "グループに招待された" app: "連携アプリからの通知" + _actions: + followBack: "フォローバック" + reply: "返信" + renote: "Renote" + _deck: alwaysShowMainColumn: "常にメインカラムを表示" columnAlign: "カラムの寄せ" diff --git a/src/client/account.ts b/src/client/account.ts index 2c5e959dec..1ba7a28908 100644 --- a/src/client/account.ts +++ b/src/client/account.ts @@ -2,7 +2,7 @@ import { get, set } from 'idb-keyval'; import { reactive } from 'vue'; import { apiUrl } from '@/config'; import { waiting } from '@/os'; -import { unisonReload } from '@/scripts/unison-reload'; +import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; // TODO: 他のタブと永続化されたstateを同期 @@ -89,18 +89,23 @@ export function updateAccount(data) { } export function refreshAccount() { - fetchAccount($i.token).then(updateAccount); + return fetchAccount($i.token).then(updateAccount); } -export async function login(token: Account['token'], showTimeline: boolean = false) { +export async function login(token: Account['token'], href?: string) { waiting(); if (_DEV_) console.log('logging as token ', token); const me = await fetchAccount(token); localStorage.setItem('account', JSON.stringify(me)); await addAccount(me.id, token); - if (showTimeline) location.href = '/'; - else unisonReload(); + if (href) { + reloadChannel.postMessage('reload'); + location.href = href; + return; + } + + unisonReload(); } // このファイルに書きたくないけどここに書かないと何故かVeturが認識しない diff --git a/src/client/init.ts b/src/client/init.ts index d8dc8d4e16..96ac6f8009 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -61,11 +61,14 @@ import * as sound from '@/scripts/sound'; import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; import { defaultStore, ColdDeviceStorage } from '@/store'; import { fetchInstance, instance } from '@/instance'; -import { makeHotkey } from './scripts/hotkey'; -import { search } from './scripts/search'; -import { getThemes } from './theme-store'; -import { initializeSw } from './scripts/initialize-sw'; -import { reloadChannel } from './scripts/unison-reload'; +import { makeHotkey } from '@/scripts/hotkey'; +import { search } from '@/scripts/search'; +import { getThemes } from '@/theme-store'; +import { initializeSw } from '@/scripts/initialize-sw'; +import { reloadChannel } from '@/scripts/unison-reload'; +import { deleteLoginId } from '@/scripts/login-id'; +import { getAccountFromId } from '@/scripts/get-account-from-id'; +import { SwMessage } from '@/sw/types'; console.info(`Misskey v${version}`); @@ -142,6 +145,25 @@ const html = document.documentElement; html.setAttribute('lang', lang); //#endregion +//#region loginId +const params = new URLSearchParams(location.href); +const loginId = params.get('loginId'); + +if (loginId) { + const target = deleteLoginId(location.toString()); + + if (!$i || $i.id !== loginId) { + const account = await getAccountFromId(loginId); + if (account) { + login(account.token, target) + } + } + + history.replaceState({ misskey: 'loginId' }, '', target) +} + +//#endregion + //#region Fetch user if ($i && $i.token) { if (_DEV_) { @@ -188,7 +210,7 @@ fetchInstance().then(() => { stream.init($i); const app = createApp(await ( - window.location.search === '?zen' ? import('@/ui/zen.vue') : + location.search === '?zen' ? import('@/ui/zen.vue') : !$i ? import('@/ui/visitor.vue') : ui === 'deck' ? import('@/ui/deck.vue') : ui === 'desktop' ? import('@/ui/desktop.vue') : @@ -217,6 +239,33 @@ components(app); await router.isReady(); +//#region Listen message from SW +navigator.serviceWorker.addEventListener('message', ev => { + if (_DEV_) { + console.log('sw msg', ev.data); + } + + const data = ev.data as SwMessage; + if (data.type !== 'order') return; + + if (data.loginId !== $i?.id) { + return getAccountFromId(data.loginId).then(account => { + if (!account) return; + return login(account.token, data.url); + }) + } + + switch (data.order) { + case 'post': + return post(data.options); + case 'push': + return router.push(data.url); + default: + return; + } +}); +//#endregion + //document.body.innerHTML = '
'; app.mount('body'); diff --git a/src/client/scripts/get-account-from-id.ts b/src/client/scripts/get-account-from-id.ts new file mode 100644 index 0000000000..52b758b39c --- /dev/null +++ b/src/client/scripts/get-account-from-id.ts @@ -0,0 +1,7 @@ +import { get } from 'idb-keyval'; + +export async function getAccountFromId(id: string) { + const accounts = await get('accounts') as { token: string; id: string; }[]; + if (!accounts) console.log('Accounts are not recorded'); + return accounts.find(e => e.id === id) +} diff --git a/src/client/scripts/initialize-sw.ts b/src/client/scripts/initialize-sw.ts index 1eef8aa0f8..785096f284 100644 --- a/src/client/scripts/initialize-sw.ts +++ b/src/client/scripts/initialize-sw.ts @@ -1,8 +1,7 @@ import { instance } from '@/instance'; import { $i } from '@/account'; -import { api, post } from '@/os'; +import { api } from '@/os'; import { lang } from '@/config'; -import { SwMessage } from '@/sw/types'; export async function initializeSw() { if (instance.swPublickey && @@ -50,18 +49,6 @@ export async function initializeSw() { } } -navigator.serviceWorker.addEventListener('message', ev => { - const data = ev.data as SwMessage; - if (data.type !== 'order') return; - - switch (data.order) { - case 'post': - return post(data.options); - default: - return; - } -}); - /** * Convert the URL safe base64 string to a Uint8Array * @param base64String base64 string diff --git a/src/client/scripts/login-id.ts b/src/client/scripts/login-id.ts new file mode 100644 index 0000000000..157b40c572 --- /dev/null +++ b/src/client/scripts/login-id.ts @@ -0,0 +1,11 @@ +export function appendLoginId(url: string, loginId: string) { + const u = new URL(url, origin); + u.searchParams.append('loginId', loginId); + return u.toString(); +} + +export function deleteLoginId(url: string) { + const u = new URL(url); + u.searchParams.delete('loginId'); + return u.toString(); +} diff --git a/src/client/sw/create-notification.ts b/src/client/sw/create-notification.ts index 9382b88345..1519ce48f3 100644 --- a/src/client/sw/create-notification.ts +++ b/src/client/sw/create-notification.ts @@ -6,7 +6,7 @@ declare var self: ServiceWorkerGlobalScope; import { getNoteSummary } from '../../misc/get-note-summary'; import getUserName from '../../misc/get-user-name'; import { swLang } from '@/sw/lang'; -import { I18n } from '@/scripts/i18n'; +import { I18n } from '../../misc/i18n'; import { pushNotificationData } from '../../types'; export async function createNotification(data: pushNotificationData) { @@ -18,107 +18,162 @@ async function composeNotification(data: pushNotificationData): Promise<[string, 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) return [t('_notification.fileUploaded'), { - body: data.body.name, - icon: data.body.url, + body: body.name, + icon: body.url, data }]; */ case 'notification': - switch (data.body.type) { + switch (body.type) { + case 'follow': + return [t('_notification.youWereFollowed'), { + body: getUserName(body.user), + icon: body.user.avatarUrl, + data, + actions: [ + { + action: 'follow', + title: t('_notification._actions.followBack') + } + ], + }]; + case 'mention': - return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), { - body: getNoteSummary(data.body.note, i18n.locale), - icon: data.body.user.avatarUrl, + return [t('_notification.youGotMention', { name: getUserName(body.user) }), { + body: getNoteSummary(body.note, i18n.locale), + icon: body.user.avatarUrl, + data, + actions: [ + { + action: 'reply', + title: t('_notification._actions.reply') + } + ], + }]; + + case 'reply': + return [t('_notification.youGotReply', { name: getUserName(body.user) }), { + body: getNoteSummary(body.note, i18n.locale), + icon: body.user.avatarUrl, + data, + actions: [ + { + action: 'reply', + title: t('_notification._actions.reply') + } + ], + }]; + + case 'renote': + return [t('_notification.youRenoted', { name: getUserName(body.user) }), { + body: getNoteSummary(body.note.renote, i18n.locale), + icon: body.user.avatarUrl, data, actions: [ { action: 'showUser', - title: 'showUser' + title: getUserName(body.user) } - ] - }]; - - case 'reply': - return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), { - body: getNoteSummary(data.body.note, i18n.locale), - icon: data.body.user.avatarUrl, - data, - }]; - - case 'renote': - return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), { - body: getNoteSummary(data.body.note, i18n.locale), - icon: data.body.user.avatarUrl, - data, + ], }]; case 'quote': - return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), { - body: getNoteSummary(data.body.note, i18n.locale), - icon: data.body.user.avatarUrl, + return [t('_notification.youGotQuote', { name: getUserName(body.user) }), { + body: getNoteSummary(body.note, i18n.locale), + icon: body.user.avatarUrl, data, + actions: [ + { + action: 'reply', + title: t('_notification._actions.reply') + }, + { + action: 'renote', + title: t('_notification._actions.renote') + } + ], }]; case 'reaction': - return [`${data.body.reaction} ${getUserName(data.body.user)}`, { - body: getNoteSummary(data.body.note, i18n.locale), - icon: data.body.user.avatarUrl, + return [`${body.reaction} ${getUserName(body.user)}`, { + body: getNoteSummary(body.note, i18n.locale), + icon: body.user.avatarUrl, data, + actions: [ + { + action: 'showUser', + title: getUserName(body.user) + } + ], }]; case 'pollVote': - return [t('_notification.youGotPoll', { name: getUserName(data.body.user) }), { - body: getNoteSummary(data.body.note, i18n.locale), - icon: data.body.user.avatarUrl, - data, - }]; - - case 'follow': - return [t('_notification.youWereFollowed'), { - body: getUserName(data.body.user), - icon: data.body.user.avatarUrl, + return [t('_notification.youGotPoll', { name: getUserName(body.user) }), { + body: getNoteSummary(body.note, i18n.locale), + icon: body.user.avatarUrl, data, }]; case 'receiveFollowRequest': return [t('_notification.youReceivedFollowRequest'), { - body: getUserName(data.body.user), - icon: data.body.user.avatarUrl, + body: getUserName(body.user), + icon: body.user.avatarUrl, data, + actions: [ + { + action: 'accept', + title: t('accept') + }, + { + action: 'reject', + title: t('reject') + } + ], }]; case 'followRequestAccepted': return [t('_notification.yourFollowRequestAccepted'), { - body: getUserName(data.body.user), - icon: data.body.user.avatarUrl, + body: getUserName(body.user), + icon: body.user.avatarUrl, data, }]; case 'groupInvited': - return [t('_notification.youWereInvitedToGroup'), { - body: data.body.group.name, + return [t('_notification.youWereInvitedToGroup', { userName: getUserName(body.user) }), { + body: body.invitation.group.name, data, + actions: [ + { + action: 'accept', + title: t('accept') + }, + { + action: 'reject', + title: t('reject') + } + ], }]; default: return null; } case 'unreadMessagingMessage': - if (data.body.groupId === null) { - return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), { - icon: data.body.user.avatarUrl, - tag: `messaging:user:${data.body.user.id}`, + if (body.groupId === null) { + return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(body.user) }), { + icon: body.user.avatarUrl, + tag: `messaging:user:${body.userId}`, data, }]; } - return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), { - icon: data.body.user.avatarUrl, - tag: `messaging:group:${data.body.group.id}`, + return [t('_notification.youGotMessagingMessageFromGroup', { name: body.group.name }), { + icon: body.user.avatarUrl, + tag: `messaging:group:${body.groupId}`, data, }]; default: diff --git a/src/client/sw/notification-read.ts b/src/client/sw/notification-read.ts index 4f9664a8fe..093322a8b7 100644 --- a/src/client/sw/notification-read.ts +++ b/src/client/sw/notification-read.ts @@ -2,12 +2,12 @@ declare var self: ServiceWorkerGlobalScope; import { get } from 'idb-keyval'; import { pushNotificationData } from '../../types'; +import { api } from './operations'; type Accounts = { [x: string]: { queue: string[], - timeout: number | null, - token: string, + timeout: number | null } }; @@ -15,14 +15,13 @@ class SwNotificationRead { private accounts: Accounts = {}; public async construct() { - const accounts = await get('accounts') as { token: string, id: string }[]; - if (!accounts) Error('Account is not recorded'); + const accounts = await get('accounts'); + if (!accounts) Error('Accounts are not recorded'); this.accounts = accounts.reduce((acc, e) => { acc[e.id] = { queue: [], - timeout: null, - token: e.token, + timeout: null }; return acc; }, {} as Accounts); @@ -36,21 +35,14 @@ class SwNotificationRead { const account = this.accounts[data.userId]; - account.queue.push(data.body.id); + account.queue.push(data.body.id as string); // 最後の呼び出しから200ms待ってまとめて処理する if (account.timeout) clearTimeout(account.timeout); account.timeout = setTimeout(() => { account.timeout = null; - console.info(account.token, account.queue); - fetch(`${location.origin}/api/notifications/read`, { - method: 'POST', - body: JSON.stringify({ - i: account.token, - notificationIds: account.queue - }) - }); + api('notifications/read', data.userId, { notificationIds: account.queue }); }, 200); } } diff --git a/src/client/sw/open-client.ts b/src/client/sw/open-client.ts deleted file mode 100644 index 5ef1cee434..0000000000 --- a/src/client/sw/open-client.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Openers - * クライアントを開く関数。 - * ユーザー、ノート、投稿フォーム、トークルーム - */ -declare var self: ServiceWorkerGlobalScope; - -import { SwMessage, swMessageOrderType } from './types'; - -// rendered acctからユーザーを開く -export async function openUser(acct: string, loginId: string) { - open('push-user', { acct }, `${origin}/@${acct}?loginId=${loginId}`, loginId) -} - -// post-formのオプションから投稿フォームを開く -export async function openPost(options: any, loginId: string) { - // Build share queries from options - let url = `${origin}/?`; - if (options.initialText) url += `text=${options.initialText}&`; - if (options.reply) url += `replyId=${options.reply.id}&`; - if (options.renote) url += `renoteId=${options.renote.id}&`; - url += `loginId=${loginId}`; - - open('post', { options }, url, loginId) -} - -async function open(order: swMessageOrderType, query: any, url: string, loginId: string) { - const client = await self.clients.matchAll({ - includeUncontrolled: true, - type: 'window' - }).then(clients => clients.length > 0 ? clients[0] : null); - - if (client) { - client.postMessage({ type: 'order', ...query, order, loginId, url } as SwMessage); - - if ('focus' in client) (client as any).focus(); - return; - } - - return self.clients.openWindow(url); -} diff --git a/src/client/sw/operations.ts b/src/client/sw/operations.ts new file mode 100644 index 0000000000..03a0b03b65 --- /dev/null +++ b/src/client/sw/operations.ts @@ -0,0 +1,72 @@ +/* + * Operations + * 各種操作 + */ +declare var self: ServiceWorkerGlobalScope; + +import { SwMessage, swMessageOrderType } from './types'; +import renderAcct from '../../misc/acct/render'; +import { getAccountFromId } from '@/scripts/get-account-from-id'; +import { appendLoginId } from '@/scripts/login-id'; + +export async function api(endpoint: string, userId: string, options: any = {}) { + 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; + }) +} + +// rendered acctからユーザーを開く +export function openUser(acct: string, loginId: string) { + return openClient('push', `/@${acct}`, loginId, { acct }) +} + +// noteIdからノートを開く +export function openNote(noteId: string, loginId: string) { + return openClient('push', `/notes/${noteId}`, loginId, { noteId }) +} + +export async function openChat(body: any, loginId: string) { + if (body.groupId === null) { + return openClient('push', `/my/messaging/${renderAcct(body.user)}`, loginId, { body }) + } else { + return openClient('push', `/my/messaging/group/${body.groupId}`, loginId, { body }) + } +} + +// post-formのオプションから投稿フォームを開く +export async function openPost(options: any, loginId: string) { + // クエリを作成しておく + let url = `/share?`; + if (options.initialText) url += `text=${options.initialText}&`; + if (options.reply) url += `replyId=${options.reply.id}&`; + if (options.renote) url += `renoteId=${options.renote.id}&`; + + return openClient('post', url, loginId, { options }) +} + +export async function openClient(order: swMessageOrderType, url: string, loginId: string, query: any = {}) { + const client = await self.clients.matchAll({ + type: 'window' + }).then(clients => clients.length > 0 ? clients[0] as WindowClient : null); + + if (client) { + client.postMessage({ type: 'order', ...query, order, loginId, url } as SwMessage); + return client; + } + + return self.clients.openWindow(appendLoginId(url, loginId)); +} diff --git a/src/client/sw/sw.ts b/src/client/sw/sw.ts index 8570c30987..8af03eb130 100644 --- a/src/client/sw/sw.ts +++ b/src/client/sw/sw.ts @@ -7,7 +7,7 @@ import { createNotification } from '@/sw/create-notification'; import { swLang } from '@/sw/lang'; import { swNotificationRead } from '@/sw/notification-read'; import { pushNotificationData } from '../../types'; -import { openUser } from './open-client'; +import * as ope from './operations'; import renderAcct from '../../misc/acct/render'; //#region Lifecycle: Install @@ -55,51 +55,125 @@ self.addEventListener('push', ev => { return createNotification(data); case 'readAllNotifications': for (const n of await self.registration.getNotifications()) { - n.close(); + 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 notification of await self.registration.getNotifications()) { - if (data.body.notificationIds.includes(notification.data.body.id)) { - notification.close(); + 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; } })); }); //#endregion //#region Notification -self.addEventListener('notificationclick', async ev => { - const { action, notification } = ev; - const data: pushNotificationData = notification.data; +self.addEventListener('notificationclick', ev => { + ev.waitUntil((async () => { - switch (action) { - case 'showUser': - switch (data.body.type) { - case 'reaction': - return openUser(renderAcct(data.body.user), data.userId); - - default: - if ('note' in data.body) { - return openUser(renderAcct(data.body.data.user), data.userId); - } - } - break; - default: + if (_DEV_) { + console.log('notificationclick', ev.action, ev.notification.data); } - // notification.close(); + const { action, notification } = ev; + const data: pushNotificationData = notification.data; + const { type, userId: id, body } = 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(renderAcct(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); + break; + case 'groupInvited': + client = await ope.openClient('push', '/my/groups', id); + break; + case 'reaction': + client = await ope.openNote(body.note.id, id); + break; + default: + if ('note' in body) { + client = await ope.openNote(body.note.id, id); + break; + } + if ('user' in body) { + client = await ope.openUser(renderAcct(body.data.user), id); + break; + } + } + } + + if (client) { + client.focus(); + } + if (type === 'notification') { + swNotificationRead.then(that => that.read(data)); + } + if (close) { + notification.close(); + } + + })()) }); self.addEventListener('notificationclose', ev => { - const { notification } = ev; - - if (!notification.title.startsWith('notification')) { - self.registration.showNotification('notificationclose', { body: `${notification?.data?.body?.id}` }); - } - const data: pushNotificationData = notification.data; + const data: pushNotificationData = ev.notification.data; if (data.type === 'notification') { swNotificationRead.then(that => that.read(data)); @@ -108,12 +182,15 @@ self.addEventListener('notificationclose', ev => { //#endregion //#region When: Caught a message from the client -self.addEventListener('message', ev => { +self.addEventListener('message', async ev => { switch (ev.data) { case 'clear': + // Cache Storage全削除 + await caches.keys() + .then(cacheNames => Promise.all( + cacheNames.map(name => caches.delete(name)) + )) return; // TODO - default: - break; } if (typeof ev.data === 'object') { diff --git a/src/client/sw/types.ts b/src/client/sw/types.ts index a3594d8a2e..14bf7af96a 100644 --- a/src/client/sw/types.ts +++ b/src/client/sw/types.ts @@ -1,4 +1,4 @@ -export type swMessageOrderType = 'post' | 'push-user' | 'push-note' | 'push-messaging-room'; +export type swMessageOrderType = 'post' | 'push'; export type SwMessage = { type: 'order'; diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts index 90510bb393..14b11f9992 100644 --- a/src/server/api/common/read-messaging-message.ts +++ b/src/server/api/common/read-messaging-message.ts @@ -1,6 +1,7 @@ import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream'; import { publishMessagingStream } from '../../../services/stream'; import { publishMessagingIndexStream } from '../../../services/stream'; +import { pushNotification } from '../../../services/push-notification'; import { User, ILocalUser, IRemoteUser } from '../../../models/entities/user'; import { MessagingMessage } from '../../../models/entities/messaging-message'; import { MessagingMessages, UserGroupJoinings, Users } from '../../../models'; @@ -12,6 +13,7 @@ import { renderReadActivity } from '../../../remote/activitypub/renderer/read'; import { renderActivity } from '../../../remote/activitypub/renderer'; import { deliver } from '../../../queue'; import orderedCollection from '../../../remote/activitypub/renderer/ordered-collection'; +import { use } from 'matter-js'; /** * Mark messages as read @@ -50,6 +52,23 @@ export async function readUserMessagingMessage( if (!await Users.getHasUnreadMessagingMessage(userId)) { // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 publishMainStream(userId, 'readAllMessagingMessages'); + pushNotification(userId, 'readAllMessagingMessages', undefined); + } else { + // そのユーザーとのメッセージで未読がなければイベント発行 + const count = await MessagingMessages.count({ + where: { + userId: otherpartyId, + recipientId: userId, + isRead: false, + }, + take: 1 + }) + + if (!count) { + pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId }); + } else { + console.log('count') + } } } @@ -104,6 +123,21 @@ export async function readGroupMessagingMessage( if (!await Users.getHasUnreadMessagingMessage(userId)) { // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 publishMainStream(userId, 'readAllMessagingMessages'); + pushNotification(userId, 'readAllMessagingMessages', undefined); + } else { + // そのグループにおいて未読がなければイベント発行 + const unreadExist = await MessagingMessages.createQueryBuilder('message') + .where(`message.groupId = :groupId`, { groupId: groupId }) + .andWhere('message.userId != :userId', { userId: userId }) + .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) + .andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない + .getOne().then(x => x != null) + + if (!unreadExist) { + pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId }); + } else { + console.log('unread exist') + } } } diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts index 70300de018..1090646924 100644 --- a/src/services/push-notification.ts +++ b/src/services/push-notification.ts @@ -11,6 +11,8 @@ type pushNotificationsTypes = { 'unreadMessagingMessage': PackedMessagingMessage; 'readNotifications': { notificationIds: string[] }; 'readAllNotifications': undefined; + 'readAllMessagingMessages': undefined; + 'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string }; }; export async function pushNotification(userId: string, type: T, body: pushNotificationsTypes[T]) { diff --git a/src/types.ts b/src/types.ts index b9b676c861..c6a40510f2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,7 +5,18 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; export type pushNotificationData = { - type: 'notification' | 'unreadMessagingMessage' | 'readNotifications' | 'readAllNotifications', - body: any, - userId: string + 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; };