From cd6428715e6780d51e8f6edf93fe7e32bd8f937b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:31:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=86=E3=82=B9=E3=83=88=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E3=82=92=E9=80=81=E4=BF=A1=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B=20(#11810)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * (add) Notification test * Update Changelog * (add) backend, frontend impl * globalEventの名前を明確にする * Run API Extractor --- CHANGELOG.md | 1 + locales/index.d.ts | 4 +++ locales/ja-JP.yml | 4 +++ .../src/models/entities/Notification.ts | 1 + .../backend/src/server/api/EndpointsModule.ts | 3 ++ packages/backend/src/server/api/endpoints.ts | 2 ++ .../notifications/test-notification.ts | 33 ++++++++++++++++++ packages/backend/src/types.ts | 2 +- .../src/components/MkNotification.vue | 4 +++ .../frontend/src/pages/settings/general.vue | 3 ++ .../src/pages/settings/notifications.vue | 6 ++++ .../frontend/src/scripts/test-notification.ts | 34 +++++++++++++++++++ packages/frontend/src/ui/_common_/common.vue | 8 +++-- packages/misskey-js/etc/misskey-js.api.md | 8 ++++- packages/misskey-js/src/api.types.ts | 1 + packages/misskey-js/src/entities.ts | 2 ++ 16 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/notifications/test-notification.ts create mode 100644 packages/frontend/src/scripts/test-notification.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cf78b54f94..03c93f70df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - 投稿フォームのプレビューの表示状態を記憶するように - AiScriptからMisskeyサーバーAPIを呼び出す際の制限を撤廃 - Playで直接投稿フォームを埋め込めるように(`Ui:C:postForm`) +- 通知をテストできるように - Enhance: ユーザーメニューでスイッチでユーザーリストに追加・削除できるように - Enhance: 自分が押したリアクションのデザインを改善 - Enhance: ノート検索にローカルのみ検索可能なオプションの追加 diff --git a/locales/index.d.ts b/locales/index.d.ts index 771d5cf872..36897285c4 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2132,6 +2132,10 @@ export interface Locale { "unreadAntennaNote": string; "emptyPushNotificationMessage": string; "achievementEarned": string; + "testNotification": string; + "checkNotificationBehavior": string; + "sendTestNotification": string; + "notificationWillBeDisplayedLikeThis": string; "_types": { "all": string; "follow": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 2b2cad8d73..f9427e13ec 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2047,6 +2047,10 @@ _notification: unreadAntennaNote: "アンテナ {name}" emptyPushNotificationMessage: "プッシュ通知の更新をしました" achievementEarned: "実績を獲得" + testNotification: "通知テスト" + checkNotificationBehavior: "通知の表示を確かめる" + sendTestNotification: "テスト通知を送信する" + notificationWillBeDisplayedLikeThis: "通知はこのように表示されます" _types: all: "すべて" diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts index 94c7084cf0..fb7f67dfd8 100644 --- a/packages/backend/src/models/entities/Notification.ts +++ b/packages/backend/src/models/entities/Notification.ts @@ -33,6 +33,7 @@ export type MiNotification = { * followRequestAccepted - 自分の送ったフォローリクエストが承認された * achievementEarned - 実績を獲得 * app - アプリ通知 + * test - テスト通知(サーバー側) */ type: typeof notificationTypes[number]; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 799ba4498d..7b9fa6c3b0 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -283,6 +283,7 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; +import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js'; import * as ep___pagePush from './endpoints/page-push.js'; import * as ep___pages_create from './endpoints/pages/create.js'; import * as ep___pages_delete from './endpoints/pages/delete.js'; @@ -629,6 +630,7 @@ const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep__ const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default }; const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default }; const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default }; +const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default }; const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default }; const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default }; const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default }; @@ -979,6 +981,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_userListTimeline, $notifications_create, $notifications_markAllAsRead, + $notifications_testNotification, $pagePush, $pages_create, $pages_delete, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 3924b43d16..a9cb7c341a 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -283,6 +283,7 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; +import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js'; import * as ep___pagePush from './endpoints/page-push.js'; import * as ep___pages_create from './endpoints/pages/create.js'; import * as ep___pages_delete from './endpoints/pages/delete.js'; @@ -627,6 +628,7 @@ const eps = [ ['notes/user-list-timeline', ep___notes_userListTimeline], ['notifications/create', ep___notifications_create], ['notifications/mark-all-as-read', ep___notifications_markAllAsRead], + ['notifications/test-notification', ep___notifications_testNotification], ['page-push', ep___pagePush], ['pages/create', ep___pages_create], ['pages/delete', ep___pages_delete], diff --git a/packages/backend/src/server/api/endpoints/notifications/test-notification.ts b/packages/backend/src/server/api/endpoints/notifications/test-notification.ts new file mode 100644 index 0000000000..04a68a8054 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notifications/test-notification.ts @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NotificationService } from '@/core/NotificationService.js'; + +export const meta = { + tags: ['notifications'], + + requireCredential: true, + + kind: 'write:notifications', +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private notificationService: NotificationService, + ) { + super(meta, paramDef, async (ps, user) => { + this.notificationService.createNotification(user.id, 'test', {}); + }); + } +} diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index a3a8e77cd0..024ba01e3b 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const; +export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index d46e8de553..ea2b6c1d4b 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
{{ i18n.ts._notification.pollEnded }} {{ i18n.ts._notification.achievementEarned }} + {{ i18n.ts._notification.testNotification }} {{ notification.header }} @@ -91,6 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.reject }}
+ {{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }} @@ -113,6 +116,7 @@ import { i18n } from '@/i18n'; import * as os from '@/os'; import { useTooltip } from '@/scripts/use-tooltip'; import { $i } from '@/account'; +import { infoImageUrl } from '@/instance'; const props = withDefaults(defineProps<{ notification: Misskey.entities.Notification; diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 85a3a2e2e3..31d5dd93ec 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -95,6 +95,8 @@ SPDX-License-Identifier: AGPL-3.0-only + + {{ i18n.ts._notification.checkNotificationBehavior }}
@@ -190,6 +192,7 @@ import { unisonReload } from '@/scripts/unison-reload'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { miLocalStorage } from '@/local-storage'; +import { testNotification } from '@/scripts/test-notification'; const lang = ref(miLocalStorage.getItem('lang')); const fontSize = ref(miLocalStorage.getItem('fontSize')); diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index b9e4c58f79..b20add724c 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -12,6 +12,11 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.markAsReadAllUnreadNotes }} + +
+ {{ i18n.ts._notification.sendTestNotification }} +
+
@@ -41,6 +46,7 @@ import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; import { notificationTypes } from '@/const'; +import { testNotification } from '@/scripts/test-notification'; let allowButton = $shallowRef>(); let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer); diff --git a/packages/frontend/src/scripts/test-notification.ts b/packages/frontend/src/scripts/test-notification.ts new file mode 100644 index 0000000000..0e8289e19e --- /dev/null +++ b/packages/frontend/src/scripts/test-notification.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import * as os from '@/os'; +import { globalEvents } from '@/events'; + +/** + * テスト通知を送信 + * + * - `client` … 通知ポップアップのみを表示 + * - `server` … サーバー側から通知を送信 + * + * @param type 通知タイプを指定 + */ +export function testNotification(type: 'client' | 'server'): void { + const notification: Misskey.entities.Notification = { + id: Math.random().toString(), + createdAt: new Date().toUTCString(), + isRead: false, + type: 'test', + }; + + switch (type) { + case 'server': + os.api('notifications/test-notification'); + break; + case 'client': + globalEvents.emit('clientNotification', notification); + break; + } +} diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 61fcb0b171..65c5dbb384 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -56,6 +56,7 @@ import { $i } from '@/account'; import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; +import { globalEvents } from '@/events'; const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue')); const XUpload = defineAsyncComponent(() => import('./upload.vue')); @@ -64,11 +65,13 @@ const dev = _DEV_; let notifications = $ref([]); -function onNotification(notification) { +function onNotification(notification: Misskey.entities.Notification, isClient: boolean = false) { if ($i.mutingNotificationTypes.includes(notification.type)) return; if (document.visibilityState === 'visible') { - useStream().send('readNotification'); + if (!isClient) { + useStream().send('readNotification'); + } notifications.unshift(notification); window.setTimeout(() => { @@ -86,6 +89,7 @@ function onNotification(notification) { if ($i) { const connection = useStream().useChannel('main', null, 'UI'); connection.on('notification', onNotification); + globalEvents.on('clientNotification', notification => onNotification(notification, true)); //#region Listen message from SW if ('serviceWorker' in navigator) { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index e3f8a65cf8..ab2cc15d52 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1915,6 +1915,10 @@ export type Endpoints = { }; res: null; }; + 'notifications/test-notification': { + req: NoParams; + res: null; + }; 'notifications/mark-all-as-read': { req: NoParams; res: null; @@ -2627,6 +2631,8 @@ type Notification_2 = { header?: string | null; body: string; icon?: string | null; +} | { + type: 'test'; }); // @public (undocumented) @@ -2842,7 +2848,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts -// src/api.types.ts:630:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts +// src/api.types.ts:631:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index 70ef570166..46d790fe31 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -534,6 +534,7 @@ export type Endpoints = { // notifications 'notifications/create': { req: { body: string; header?: string | null; icon?: string | null; }; res: null; }; + 'notifications/test-notification': { req: NoParams; res: null; }; 'notifications/mark-all-as-read': { req: NoParams; res: null; }; // page-push diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 64742fa5b6..3782d81c2e 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -257,6 +257,8 @@ export type Notification = { header?: string | null; body: string; icon?: string | null; +} | { + type: 'test'; }); export type MessagingMessage = {