From c2370a1be631355e709c47d3b5e9469906116b84 Mon Sep 17 00:00:00 2001 From: Shun Sakai Date: Thu, 27 Jul 2023 14:31:52 +0900 Subject: chore: 著作権とライセンスについての情報を各ファイルに追加する (#11348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Add the SPDX information to each file Add copyright and licensing information as defined in version 3.0 of the REUSE Specification. * tweak format --------- Co-authored-by: syuilo --- packages/backend/src/server/api/EndpointsModule.ts | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'packages/backend/src/server/api/EndpointsModule.ts') diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 4e6bc46e67..3be6524689 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; -- cgit v1.2.3-freya From 70a78009270df3ff9a76e84a4e2dd0837a1b5f46 Mon Sep 17 00:00:00 2001 From: GrapeApple0 <84321396+GrapeApple0@users.noreply.github.com> Date: Mon, 21 Aug 2023 20:23:09 +0900 Subject: プロフィールにその人が作ったPlayの一覧出せるように (#11445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * プロフィールにその人が作ったPlayの一覧出せるように * Update CHANGELOG.md * playの公開範囲を設定できるように * 間違えて変更してしまったのを修正 * Update packages/frontend/src/pages/flash/flash-edit.vue * Update packages/frontend/src/pages/flash/flash-edit.vue * tweak * Update packages/backend/migration/1690796169261-play-visibility.js * Update packages/backend/src/models/entities/Flash.ts * Update packages/backend/src/models/entities/Flash.ts * Update Flash.ts --------- Co-authored-by: syuilo --- CHANGELOG.md | 1 + locales/en-US.yml | 1 + locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + .../migration/1690796169261-play-visibility.js | 15 ++++++ packages/backend/src/models/entities/Flash.ts | 9 ++++ packages/backend/src/server/api/EndpointsModule.ts | 4 ++ packages/backend/src/server/api/endpoints.ts | 2 + .../src/server/api/endpoints/flash/update.ts | 1 + .../src/server/api/endpoints/users/flashs.ts | 62 ++++++++++++++++++++++ packages/frontend/src/pages/flash/flash-edit.vue | 8 +++ packages/frontend/src/pages/user/flashs.vue | 31 +++++++++++ packages/frontend/src/pages/user/index.vue | 6 +++ packages/misskey-js/src/api.types.ts | 1 + 14 files changed, 143 insertions(+) create mode 100644 packages/backend/migration/1690796169261-play-visibility.js create mode 100644 packages/backend/src/server/api/endpoints/users/flashs.ts create mode 100644 packages/frontend/src/pages/user/flashs.vue (limited to 'packages/backend/src/server/api/EndpointsModule.ts') diff --git a/CHANGELOG.md b/CHANGELOG.md index a768f0ecb2..88c6c36252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - チャンネルをセンシティブ指定できるようになりました ### Client +- プロフィールにその人が作ったPlayの一覧出せるように - メニューのスイッチの動作を改善 - 絵文字ピッカーの検索の表示件数を100件に増加 - 投稿フォームのプレビューの表示状態を記憶するように diff --git a/locales/en-US.yml b/locales/en-US.yml index 097aba76ba..320a2dbcbc 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -680,6 +680,7 @@ createNewClip: "Create new clip" unclip: "Unclip" confirmToUnclipAlreadyClippedNote: "This note is already part of the \"{name}\" clip. Do you want to remove it from this clip instead?" public: "Public" +private: "Private" i18nInfo: "Misskey is being translated into various languages by volunteers. You can help at {link}." manageAccessTokens: "Manage access tokens" accountInfo: "Account Info" diff --git a/locales/index.d.ts b/locales/index.d.ts index d7c85b9597..1f25edd0ef 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -684,6 +684,7 @@ export interface Locale { "unclip": string; "confirmToUnclipAlreadyClippedNote": string; "public": string; + "private": string; "i18nInfo": string; "manageAccessTokens": string; "accountInfo": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0dfaa43653..2e0e64bbef 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -681,6 +681,7 @@ createNewClip: "新しいクリップを作成" unclip: "クリップ解除" confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか?" public: "パブリック" +private: "非公開" i18nInfo: "Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。" manageAccessTokens: "アクセストークンの管理" accountInfo: "アカウント情報" diff --git a/packages/backend/migration/1690796169261-play-visibility.js b/packages/backend/migration/1690796169261-play-visibility.js new file mode 100644 index 0000000000..c57fa7a109 --- /dev/null +++ b/packages/backend/migration/1690796169261-play-visibility.js @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class PlayVisibility1689102832143 { + name = 'PlayVisibility1690796169261' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "public"."flash" ADD "visibility" character varying(512) DEFAULT 'public'`, undefined); + } + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "public"."flash" DROP COLUMN "visibility"`, undefined); + } +} diff --git a/packages/backend/src/models/entities/Flash.ts b/packages/backend/src/models/entities/Flash.ts index af4553ee9b..38e023332e 100644 --- a/packages/backend/src/models/entities/Flash.ts +++ b/packages/backend/src/models/entities/Flash.ts @@ -61,4 +61,13 @@ export class MiFlash { default: 0, }) public likedCount: number; + + /** + * public ... 公開 + * private ... プロフィールには表示しない + */ + @Column('varchar', { + length: 512, default: 'public', + }) + public visibility: 'public' | 'private'; } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 3be6524689..799ba4498d 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -336,6 +336,7 @@ import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; +import * as ep___users_flashs from './endpoints/users/flashs.js'; import * as ep___users_reactions from './endpoints/users/reactions.js'; import * as ep___users_recommendation from './endpoints/users/recommendation.js'; import * as ep___users_relation from './endpoints/users/relation.js'; @@ -681,6 +682,7 @@ const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite' const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default }; const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default }; const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default }; +const $users_flashs: Provider = { provide: 'ep:users/flashs', useClass: ep___users_flashs.default }; const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default }; const $users_recommendation: Provider = { provide: 'ep:users/recommendation', useClass: ep___users_recommendation.default }; const $users_relation: Provider = { provide: 'ep:users/relation', useClass: ep___users_relation.default }; @@ -1030,6 +1032,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_create_from_public, $users_notes, $users_pages, + $users_flashs, $users_reactions, $users_recommendation, $users_relation, @@ -1371,6 +1374,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_create_from_public, $users_notes, $users_pages, + $users_flashs, $users_reactions, $users_recommendation, $users_relation, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 5fef975fa6..3924b43d16 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -336,6 +336,7 @@ import * as ep___users_lists_create_from_public from './endpoints/users/lists/cr import * as ep___users_lists_update from './endpoints/users/lists/update.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; +import * as ep___users_flashs from './endpoints/users/flashs.js'; import * as ep___users_reactions from './endpoints/users/reactions.js'; import * as ep___users_recommendation from './endpoints/users/recommendation.js'; import * as ep___users_relation from './endpoints/users/relation.js'; @@ -679,6 +680,7 @@ const eps = [ ['users/lists/create-from-public', ep___users_lists_create_from_public], ['users/notes', ep___users_notes], ['users/pages', ep___users_pages], + ['users/flashs', ep___users_flashs], ['users/reactions', ep___users_reactions], ['users/recommendation', ep___users_recommendation], ['users/relation', ep___users_relation], diff --git a/packages/backend/src/server/api/endpoints/flash/update.ts b/packages/backend/src/server/api/endpoints/flash/update.ts index 04ed79350e..9da29b50ea 100644 --- a/packages/backend/src/server/api/endpoints/flash/update.ts +++ b/packages/backend/src/server/api/endpoints/flash/update.ts @@ -49,6 +49,7 @@ export const paramDef = { permissions: { type: 'array', items: { type: 'string', } }, + visibility: { type: 'string', enum: ['public', 'private'] }, }, required: ['flashId', 'title', 'summary', 'script', 'permissions'], } as const; diff --git a/packages/backend/src/server/api/endpoints/users/flashs.ts b/packages/backend/src/server/api/endpoints/users/flashs.ts new file mode 100644 index 0000000000..4e267f9003 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/flashs.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; +import type { FlashsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['users', 'flashs'], + + description: 'Show all flashs this user created.', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Flash', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.flashsRepository) + private flashsRepository: FlashsRepository, + + private flashEntityService: FlashEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId) + .andWhere('flash.userId = :userId', { userId: ps.userId }) + .andWhere('flash.visibility = \'public\''); + + const flashs = await query + .limit(ps.limit) + .getMany(); + + return await this.flashEntityService.packMany(flashs); + }); + } +} diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index fecf6edd0c..414fe4836c 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -23,6 +23,11 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.show }} {{ i18n.ts.delete }} + + + + + @@ -36,6 +41,7 @@ import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import MkTextarea from '@/components/MkTextarea.vue'; import MkInput from '@/components/MkInput.vue'; +import MkSelect from '@/components/MkSelect.vue'; import { useRouter } from '@/router'; const PRESET_DEFAULT = `/// @ 0.15.0 @@ -358,6 +364,7 @@ const props = defineProps<{ }>(); let flash = $ref(null); +let visibility = $ref('public'); if (props.id) { flash = await os.api('flash/show', { @@ -402,6 +409,7 @@ async function save() { summary, permissions, script, + visibility, }); } else { const created = await os.apiWithDialog('flash/create', { diff --git a/packages/frontend/src/pages/user/flashs.vue b/packages/frontend/src/pages/user/flashs.vue new file mode 100644 index 0000000000..b91ecb8de6 --- /dev/null +++ b/packages/frontend/src/pages/user/flashs.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index 314548fee3..c014897a24 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -42,6 +43,7 @@ const XReactions = defineAsyncComponent(() => import('./reactions.vue')); const XClips = defineAsyncComponent(() => import('./clips.vue')); const XLists = defineAsyncComponent(() => import('./lists.vue')); const XPages = defineAsyncComponent(() => import('./pages.vue')); +const XFlashs = defineAsyncComponent(() => import('./flashs.vue')); const XGallery = defineAsyncComponent(() => import('./gallery.vue')); const props = withDefaults(defineProps<{ @@ -103,6 +105,10 @@ const headerTabs = $computed(() => user ? [{ key: 'pages', title: i18n.ts.pages, icon: 'ti ti-news', +}, { + key: 'flashs', + title: 'Play', + icon: 'ti ti-player-play', }, { key: 'gallery', title: i18n.ts.gallery, diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index de9af99168..70ef570166 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -621,6 +621,7 @@ export type Endpoints = { 'users/lists/update': { req: { listId: UserList['id']; name: string; }; res: UserList; }; 'users/notes': { req: { userId: User['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; }; 'users/pages': { req: TODO; res: TODO; }; + 'users/flashs': { req: TODO; res: TODO; }; 'users/recommendation': { req: TODO; res: TODO; }; 'users/relation': { req: TODO; res: TODO; }; 'users/report-abuse': { req: TODO; res: TODO; }; -- cgit v1.2.3-freya From cd6428715e6780d51e8f6edf93fe7e32bd8f937b Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:31:50 +0900 Subject: feat: テスト通知を送信できるようにする (#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 +++ .../backend/src/models/entities/Notification.ts | 1 + packages/backend/src/server/api/EndpointsModule.ts | 3 ++ packages/backend/src/server/api/endpoints.ts | 2 ++ .../endpoints/notifications/test-notification.ts | 33 +++++++++++++++++++++ packages/backend/src/types.ts | 2 +- .../frontend/src/components/MkNotification.vue | 4 +++ packages/frontend/src/pages/settings/general.vue | 3 ++ .../frontend/src/pages/settings/notifications.vue | 6 ++++ packages/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 (limited to 'packages/backend/src/server/api/EndpointsModule.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 = { -- cgit v1.2.3-freya From e3f151e2307e4c0d7b9cdfc7deba2ff028adce03 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 21 Sep 2023 18:48:15 +0900 Subject: feat: 指定したユーザーの投稿通知 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #11499 --- CHANGELOG.md | 1 + locales/index.d.ts | 3 + locales/ja-JP.yml | 3 + .../migration/1695288787870-following-notify.js | 13 +++ packages/backend/src/core/NoteCreateService.ts | 19 +++- .../src/core/entities/NotificationEntityService.ts | 2 +- .../backend/src/core/entities/UserEntityService.ts | 14 +-- packages/backend/src/models/Following.ts | 7 ++ packages/backend/src/models/Notification.ts | 12 --- packages/backend/src/models/json-schema/user.ts | 4 + packages/backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../src/server/api/endpoints/following/create.ts | 2 +- .../server/api/endpoints/following/invalidate.ts | 4 +- .../src/server/api/endpoints/following/update.ts | 107 +++++++++++++++++++++ packages/backend/src/types.ts | 17 +++- packages/backend/test/e2e/users.ts | 2 +- packages/frontend/.storybook/fakes.ts | 1 + .../frontend/src/components/MkNotification.vue | 5 + packages/frontend/src/const.ts | 2 +- packages/frontend/src/scripts/get-user-menu.ts | 18 ++++ packages/misskey-js/etc/misskey-js.api.md | 10 +- packages/misskey-js/src/consts.ts | 2 +- packages/misskey-js/src/entities.ts | 8 +- packages/sw/src/scripts/create-notification.ts | 7 ++ 25 files changed, 238 insertions(+), 31 deletions(-) create mode 100644 packages/backend/migration/1695288787870-following-notify.js create mode 100644 packages/backend/src/server/api/endpoints/following/update.ts (limited to 'packages/backend/src/server/api/EndpointsModule.ts') diff --git a/CHANGELOG.md b/CHANGELOG.md index 18c5d5f558..082b448c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Feat: 二要素認証のバックアップコードが生成されるようになりました - ref. https://github.com/MisskeyIO/misskey/pull/121 - Feat: 二要素認証でパスキーをサポートするようになりました +- Feat: 指定したユーザーが投稿したときに通知できるようになりました - Feat: プロフィールでのリンク検証 - Feat: 通知をテストできるようになりました - Feat: PWAのアイコンが設定できるようになりました diff --git a/locales/index.d.ts b/locales/index.d.ts index c4379409fd..784f53355a 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1117,6 +1117,8 @@ export interface Locale { "pinnedList": string; "keepScreenOn": string; "verifiedLink": string; + "notifyNotes": string; + "unnotifyNotes": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; @@ -2150,6 +2152,7 @@ export interface Locale { "youReceivedFollowRequest": string; "yourFollowRequestAccepted": string; "pollEnded": string; + "newNote": string; "unreadAntennaNote": string; "emptyPushNotificationMessage": string; "achievementEarned": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f39bc52822..a7a6200472 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1114,6 +1114,8 @@ loadConversation: "会話を見る" pinnedList: "ピン留めされたリスト" keepScreenOn: "デバイスの画面を常にオンにする" verifiedLink: "このリンク先の所有者であることが確認されました" +notifyNotes: "投稿を通知" +unnotifyNotes: "投稿の通知を解除" _announcement: forExistingUsers: "既存ユーザーのみ" @@ -2064,6 +2066,7 @@ _notification: youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" pollEnded: "アンケートの結果が出ました" + newNote: "新しい投稿" unreadAntennaNote: "アンテナ {name}" emptyPushNotificationMessage: "プッシュ通知の更新をしました" achievementEarned: "実績を獲得" diff --git a/packages/backend/migration/1695288787870-following-notify.js b/packages/backend/migration/1695288787870-following-notify.js new file mode 100644 index 0000000000..e7e2194b15 --- /dev/null +++ b/packages/backend/migration/1695288787870-following-notify.js @@ -0,0 +1,13 @@ +export class FollowingNotify1695288787870 { + name = 'FollowingNotify1695288787870' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "following" ADD "notify" character varying(32)`); + await queryRunner.query(`CREATE INDEX "IDX_5108098457488634a4768e1d12" ON "following" ("notify") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_5108098457488634a4768e1d12"`); + await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "notify"`); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 4b0d818862..972319ddcf 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -185,6 +185,9 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.noteThreadMutingsRepository) private noteThreadMutingsRepository: NoteThreadMutingsRepository, + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, @@ -505,6 +508,20 @@ export class NoteCreateService implements OnApplicationShutdown { this.saveReply(data.reply, note); } + if (data.reply == null) { + this.followingsRepository.findBy({ + followeeId: user.id, + notify: 'normal', + }).then(followings => { + for (const following of followings) { + this.notificationService.createNotification(following.followerId, 'note', { + notifierId: user.id, + noteId: note.id, + }); + } + }); + } + // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) { if (!user.isBot) this.incRenoteCount(data.renote); diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 75a2f62f6e..3ee7c91f3a 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -20,7 +20,7 @@ import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); @Injectable() export class NotificationEntityService implements OnModuleInit { diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 7bef410bf9..3dd64ce625 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -146,15 +146,14 @@ export class UserEntityService implements OnModuleInit { @bindThis public async getRelation(me: MiUser['id'], target: MiUser['id']) { + const following = await this.followingsRepository.findOneBy({ + followerId: me, + followeeId: target, + }); return awaitAll({ id: target, - isFollowing: this.followingsRepository.count({ - where: { - followerId: me, - followeeId: target, - }, - take: 1, - }).then(n => n > 0), + following, + isFollowing: following != null, isFollowed: this.followingsRepository.count({ where: { followerId: target, @@ -486,6 +485,7 @@ export class UserEntityService implements OnModuleInit { isBlocked: relation.isBlocked, isMuted: relation.isMuted, isRenoteMuted: relation.isRenoteMuted, + notify: relation.following?.notify ?? 'none', } : {}), } as Promiseable> as Promiseable>; diff --git a/packages/backend/src/models/Following.ts b/packages/backend/src/models/Following.ts index 05b729412f..8c9f965fad 100644 --- a/packages/backend/src/models/Following.ts +++ b/packages/backend/src/models/Following.ts @@ -45,6 +45,13 @@ export class MiFollowing { @JoinColumn() public follower: MiUser | null; + @Index() + @Column('varchar', { + length: 32, + nullable: true, + }) + public notify: 'normal' | null; + //#region Denormalized fields @Index() @Column('varchar', { diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index fb7f67dfd8..c0a9df2e23 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -22,18 +22,6 @@ export type MiNotification = { /** * 通知の種類。 - * follow - フォローされた - * mention - 投稿で自分が言及された - * reply - 投稿に返信された - * renote - 投稿がRenoteされた - * quote - 投稿が引用Renoteされた - * reaction - 投稿にリアクションされた - * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した - * receiveFollowRequest - フォローリクエストされた - * followRequestAccepted - 自分の送ったフォローリクエストが承認された - * achievementEarned - 実績を獲得 - * app - アプリ通知 - * test - テスト通知(サーバー側) */ type: typeof notificationTypes[number]; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 8d0e4e72ed..f15b225a30 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -273,6 +273,10 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'string', nullable: false, optional: true, }, + notify: { + type: 'string', + nullable: false, optional: true, + }, //#endregion }, } as const; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 7b9fa6c3b0..41a11bfb19 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -160,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js'; import * as ep___federation_stats from './endpoints/federation/stats.js'; import * as ep___following_create from './endpoints/following/create.js'; import * as ep___following_delete from './endpoints/following/delete.js'; +import * as ep___following_update from './endpoints/following/update.js'; import * as ep___following_invalidate from './endpoints/following/invalidate.js'; import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; @@ -507,6 +508,7 @@ const $federation_users: Provider = { provide: 'ep:federation/users', useClass: const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default }; const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default }; const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default }; +const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default }; const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default }; const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default }; const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default }; @@ -858,6 +860,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $federation_stats, $following_create, $following_delete, + $following_update, $following_invalidate, $following_requests_accept, $following_requests_cancel, @@ -1203,6 +1206,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $federation_stats, $following_create, $following_delete, + $following_update, $following_invalidate, $following_requests_accept, $following_requests_cancel, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index a9cb7c341a..ab20a708ef 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -160,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js'; import * as ep___federation_stats from './endpoints/federation/stats.js'; import * as ep___following_create from './endpoints/following/create.js'; import * as ep___following_delete from './endpoints/following/delete.js'; +import * as ep___following_update from './endpoints/following/update.js'; import * as ep___following_invalidate from './endpoints/following/invalidate.js'; import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; @@ -505,6 +506,7 @@ const eps = [ ['federation/stats', ep___federation_stats], ['following/create', ep___following_create], ['following/delete', ep___following_delete], + ['following/update', ep___following_update], ['following/invalidate', ep___following_invalidate], ['following/requests/accept', ep___following_requests_accept], ['following/requests/cancel', ep___following_requests_cancel], diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index d80c853672..e0e7fed87a 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -19,7 +19,7 @@ export const meta = { limit: { duration: ms('1hour'), - max: 50, + max: 100, }, requireCredential: true, diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index 147d16556f..53ef925b2f 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -29,7 +29,7 @@ export const meta = { noSuchUser: { message: 'No such user.', code: 'NO_SUCH_USER', - id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8', + id: 'b77e6ae6-a3e5-40da-9cc8-c240115479cc', }, followerIsYourself: { @@ -41,7 +41,7 @@ export const meta = { notFollowing: { message: 'The other use is not following you.', code: 'NOT_FOLLOWING', - id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09', + id: '918faac3-074f-41ae-9c43-ed5d2946770d', }, }, diff --git a/packages/backend/src/server/api/endpoints/following/update.ts b/packages/backend/src/server/api/endpoints/following/update.ts new file mode 100644 index 0000000000..25f393e517 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/following/update.ts @@ -0,0 +1,107 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { FollowingsRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['following', 'users'], + + limit: { + duration: ms('1hour'), + max: 100, + }, + + requireCredential: true, + + kind: 'write:following', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '14318698-f67e-492a-99da-5353a5ac52be', + }, + + followeeIsYourself: { + message: 'Followee is yourself.', + code: 'FOLLOWEE_IS_YOURSELF', + id: '4c4cbaf9-962a-463b-8418-a5e365dbf2eb', + }, + + notFollowing: { + message: 'You are not following that user.', + code: 'NOT_FOLLOWING', + id: 'b8dc75cf-1cb5-46c9-b14b-5f1ffbd782c9', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + notify: { type: 'string', enum: ['normal', 'none'] }, + }, + required: ['userId', 'notify'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + const follower = me; + + // Check if the follower is yourself + if (me.id === ps.userId) { + throw new ApiError(meta.errors.followeeIsYourself); + } + + // Get followee + const followee = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check not following + const exist = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, + }); + + if (exist == null) { + throw new ApiError(meta.errors.notFollowing); + } + + await this.followingsRepository.update({ + id: exist.id, + }, { + notify: ps.notify === 'none' ? null : ps.notify, + }); + + return await this.userEntityService.pack(follower.id, me); + }); + } +} diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 024ba01e3b..0a28d88d08 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -3,7 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const; +/** + * note - 通知オンにしているユーザーが投稿した + * follow - フォローされた + * mention - 投稿で自分が言及された + * reply - 投稿に返信された + * renote - 投稿がRenoteされた + * quote - 投稿が引用Renoteされた + * reaction - 投稿にリアクションされた + * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した + * receiveFollowRequest - フォローリクエストされた + * followRequestAccepted - 自分の送ったフォローリクエストが承認された + * achievementEarned - 実績を獲得 + * app - アプリ通知 + * test - テスト通知(サーバー側) + */ +export const notificationTypes = ['note', '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/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 13cea0cfc2..e4ecd6e398 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -494,7 +494,7 @@ describe('ユーザー', () => { { parameters: (): object => ({ mutedWords: [] }) }, { parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) }, { parameters: (): object => ({ mutedInstances: [] }) }, - { parameters: (): object => ({ mutingNotificationTypes: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) }, + { parameters: (): object => ({ mutingNotificationTypes: ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) }, { parameters: (): object => ({ mutingNotificationTypes: [] }) }, { parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) }, { parameters: (): object => ({ emailNotificationTypes: [] }) }, diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index 2bda89196a..811c243926 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -120,6 +120,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi updatedAt: null, uri: null, url: null, + notify: 'none', }; } diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index f71834b099..99443a6409 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+ @@ -47,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._notification.pollEnded }} + {{ i18n.ts._notification.newNote }}: {{ notification.note.user.name ?? notification.note.user.username }} {{ i18n.ts._notification.achievementEarned }} {{ i18n.ts._notification.testNotification }} @@ -73,6 +75,9 @@ SPDX-License-Identifier: AGPL-3.0-only + + + diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 19bad95c87..15038b1063 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -54,7 +54,7 @@ https://github.com/sindresorhus/file-type/blob/main/core.js https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers */ -export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const; +export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const ROLE_POLICIES = [ diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 1561daa6d9..128cbafb15 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -80,6 +80,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router }); } + async function toggleNotify() { + os.apiWithDialog('following/update', { + userId: user.id, + notify: user.notify === 'normal' ? 'none' : 'normal', + }).then(() => { + user.notify = user.notify === 'normal' ? 'none' : 'normal'; + }); + } + function reportAbuse() { os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { user: user, @@ -270,6 +279,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router }]); } + // フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため + //if (user.isFollowing) { + menu = menu.concat([{ + icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off', + text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes, + action: toggleNotify, + }]); + //} + menu = menu.concat([null, { icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index fd2d0ced02..d72652bd92 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2609,7 +2609,12 @@ type Notification_2 = { userId: User['id']; note: Note; } | { - type: 'pollVote'; + type: 'note'; + user: User; + userId: User['id']; + note: Note; +} | { + type: 'pollEnded'; user: User; userId: User['id']; note: Note; @@ -2640,7 +2645,7 @@ type Notification_2 = { }); // @public (undocumented) -export const notificationTypes: readonly ["follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app"]; +export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app"]; // @public (undocumented) type OriginType = 'combined' | 'local' | 'remote'; @@ -2810,6 +2815,7 @@ type UserDetailed = UserLite & { updatedAt: DateString | null; uri: string | null; url: string | null; + notify: 'normal' | 'none'; }; // @public (undocumented) diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 2b144ab4a4..6cf6dc07e7 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -1,4 +1,4 @@ -export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const; +export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 018210c96b..9a0114d71c 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -70,6 +70,7 @@ export type UserDetailed = UserLite & { updatedAt: DateString | null; uri: string | null; url: string | null; + notify: 'normal' | 'none'; }; export type UserGroup = TODO; @@ -233,7 +234,12 @@ export type Notification = { userId: User['id']; note: Note; } | { - type: 'pollVote'; + type: 'note'; + user: User; + userId: User['id']; + note: Note; +} | { + type: 'pollEnded'; user: User; userId: User['id']; note: Note; diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts index 2b7f91a741..f33ab1c33c 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -134,6 +134,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif ], }]; + case 'note': + return [t('_notification.newNote') + ': ' + getUserName(data.body.user), { + body: data.body.note.text ?? '', + icon: data.body.user.avatarUrl, + data, + }]; + case 'reaction': { let reaction = data.body.reaction; let badge: string | undefined; -- cgit v1.2.3-freya