From e3f151e2307e4c0d7b9cdfc7deba2ff028adce03 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 21 Sep 2023 18:48:15 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8C=87=E5=AE=9A=E3=81=97=E3=81=9F?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=AE=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E9=80=9A=E7=9F=A5?= 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 + .../1695288787870-following-notify.js | 13 +++ .../backend/src/core/NoteCreateService.ts | 19 +++- .../entities/NotificationEntityService.ts | 2 +- .../src/core/entities/UserEntityService.ts | 14 +-- packages/backend/src/models/Following.ts | 7 ++ packages/backend/src/models/Notification.ts | 12 -- .../backend/src/models/json-schema/user.ts | 4 + .../backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../server/api/endpoints/following/create.ts | 2 +- .../api/endpoints/following/invalidate.ts | 4 +- .../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 + .../src/components/MkNotification.vue | 5 + packages/frontend/src/const.ts | 2 +- .../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 +- .../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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 18c5d5f55..082b448c2 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 c4379409f..784f53355 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 f39bc5282..a7a620047 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 000000000..e7e2194b1 --- /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 4b0d81886..972319ddc 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 75a2f62f6..3ee7c91f3 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 7bef410bf..3dd64ce62 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 05b729412..8c9f965fa 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 fb7f67dfd..c0a9df2e2 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 8d0e4e72e..f15b225a3 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 7b9fa6c3b..41a11bfb1 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 a9cb7c341..ab20a708e 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 d80c85367..e0e7fed87 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 147d16556..53ef925b2 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 000000000..25f393e51 --- /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 024ba01e3..0a28d88d0 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 13cea0cfc..e4ecd6e39 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 2bda89196..811c24392 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 f71834b09..99443a640 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 19bad95c8..15038b106 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 1561daa6d..128cbafb1 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 fd2d0ced0..d72652bd9 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 2b144ab4a..6cf6dc07e 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 018210c96..9a0114d71 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 2b7f91a74..f33ab1c33 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;