diff --git a/locales/index.d.ts b/locales/index.d.ts index 0ae188f1f7..3389c78989 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5222,6 +5222,14 @@ export interface Locale extends ILocale { * 注意事項を理解した上でオンにします。 */ "acknowledgeNotesAndEnable": string; + /** + * 既読をリセット + */ + "resetReads": string; + /** + * 「{x}」の既読をリセットしますか? + */ + "resetReadsAreYouSure": ParameterizedString<"x">; "_accountSettings": { /** * コンテンツの表示にログインを必須にする @@ -9910,6 +9918,14 @@ export interface Locale extends ILocale { * ユーザーのお知らせを削除 */ "deleteUserAnnouncement": string; + /** + * 全体のお知らせの既読をリセット + */ + "resetReadsForGlobalAnnouncement": string; + /** + * ユーザーのお知らせの既読をリセット + */ + "resetReadsForUserAnnouncement": string; /** * パスワードをリセット */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1b59708d85..c3e160629c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1301,6 +1301,8 @@ lockdown: "ロックダウン" pleaseSelectAccount: "アカウントを選択してください" availableRoles: "利用可能なロール" acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。" +resetReads: "既読をリセット" +resetReadsAreYouSure: "「{x}」の既読をリセットしますか?" _accountSettings: requireSigninToViewContents: "コンテンツの表示にログインを必須にする" @@ -2627,6 +2629,8 @@ _moderationLogTypes: updateUserAnnouncement: "ユーザーのお知らせを更新" deleteGlobalAnnouncement: "全体のお知らせを削除" deleteUserAnnouncement: "ユーザーのお知らせを削除" + resetReadsForGlobalAnnouncement: "全体のお知らせの既読をリセット" + resetReadsForUserAnnouncement: "ユーザーのお知らせの既読をリセット" resetPassword: "パスワードをリセット" suspendRemoteInstance: "リモートサーバーを停止" unsuspendRemoteInstance: "リモートサーバーを再開" diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts index a9f6731977..7239a6a788 100644 --- a/packages/backend/src/core/AnnouncementService.ts +++ b/packages/backend/src/core/AnnouncementService.ts @@ -179,6 +179,32 @@ export class AnnouncementService { } } + @bindThis + public async resetReads(announcementId: MiAnnouncement['id'], moderator?: MiUser): Promise { + await this.announcementReadsRepository.delete({ + announcementId: announcementId, + }); + + if (moderator) { + const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId }); + if (announcement.userId) { + const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId }); + this.moderationLogService.log(moderator, 'resetReadsForUserAnnouncement', { + announcementId: announcement.id, + announcement: announcement, + userId: announcement.userId, + userUsername: user.username, + userHost: user.host, + }); + } else { + this.moderationLogService.log(moderator, 'resetReadsForGlobalAnnouncement', { + announcementId: announcement.id, + announcement: announcement, + }); + } + } + } + @bindThis public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise> { const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId }); diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 5bb194313d..400c1b89dd 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -23,6 +23,7 @@ import * as ep___admin_ad_update from './endpoints/admin/ad/update.js'; import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js'; import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js'; import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js'; +import * as ep___admin_announcements_resetReads from './endpoints/admin/announcements/reset-reads.js'; import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js'; import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js'; import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js'; @@ -411,6 +412,7 @@ const $admin_ad_update: Provider = { provide: 'ep:admin/ad/update', useClass: ep const $admin_announcements_create: Provider = { provide: 'ep:admin/announcements/create', useClass: ep___admin_announcements_create.default }; const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default }; const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default }; +const $admin_announcements_resetReads: Provider = { provide: 'ep:admin/announcements/reset-reads', useClass: ep___admin_announcements_resetReads.default }; const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default }; const $admin_avatarDecorations_create: Provider = { provide: 'ep:admin/avatar-decorations/create', useClass: ep___admin_avatarDecorations_create.default }; const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-decorations/delete', useClass: ep___admin_avatarDecorations_delete.default }; @@ -803,6 +805,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_announcements_create, $admin_announcements_delete, $admin_announcements_list, + $admin_announcements_resetReads, $admin_announcements_update, $admin_avatarDecorations_create, $admin_avatarDecorations_delete, @@ -1189,6 +1192,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_announcements_create, $admin_announcements_delete, $admin_announcements_list, + $admin_announcements_resetReads, $admin_announcements_update, $admin_avatarDecorations_create, $admin_avatarDecorations_delete, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 15809b2678..dbc95e46d3 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -28,6 +28,7 @@ import * as ep___admin_ad_update from './endpoints/admin/ad/update.js'; import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js'; import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js'; import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js'; +import * as ep___admin_announcements_resetReads from './endpoints/admin/announcements/reset-reads.js'; import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js'; import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js'; import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js'; @@ -415,6 +416,7 @@ const eps = [ ['admin/announcements/create', ep___admin_announcements_create], ['admin/announcements/delete', ep___admin_announcements_delete], ['admin/announcements/list', ep___admin_announcements_list], + ['admin/announcements/reset-reads', ep___admin_announcements_resetReads], ['admin/announcements/update', ep___admin_announcements_update], ['admin/avatar-decorations/create', ep___admin_avatarDecorations_create], ['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete], diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 7596bf44e3..9d4a3c3b9b 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -53,6 +53,37 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + icon: { + type: 'string', + optional: false, nullable: false, + enum: ['info', 'warning', 'error', 'success'], + }, + display: { + type: 'string', + optional: false, nullable: false, + enum: ['normal', 'banner', 'dialog'], + }, + isActive: { + type: 'boolean', + optional: false, nullable: false, + }, + forExistingUsers: { + type: 'boolean', + optional: false, nullable: false, + }, + silence: { + type: 'boolean', + optional: false, nullable: false, + }, + needConfirmationToRead: { + type: 'boolean', + optional: false, nullable: false, + }, + userId: { + type: 'string', + optional: false, nullable: true, + format: 'id', + }, reads: { type: 'number', optional: false, nullable: false, @@ -125,7 +156,7 @@ export default class extends Endpoint { // eslint- silence: announcement.silence, needConfirmationToRead: announcement.needConfirmationToRead, userId: announcement.userId, - reads: reads.get(announcement)!, + reads: reads.get(announcement) ?? 0, })); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/reset-reads.ts b/packages/backend/src/server/api/endpoints/admin/announcements/reset-reads.ts new file mode 100644 index 0000000000..fb268957a8 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/announcements/reset-reads.ts @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { AnnouncementsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { AnnouncementService } from '@/core/AnnouncementService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:announcements', + + errors: { + noSuchAnnouncement: { + message: 'No such announcement.', + code: 'NO_SUCH_ANNOUNCEMENT', + id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + announcementId: { type: 'string', format: 'misskey:id' }, + }, + required: ['announcementId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, + + private announcementService: AnnouncementService, + ) { + super(meta, paramDef, async (ps, me) => { + const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId }); + + if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); + + await this.announcementService.resetReads(announcement.id, me); + }); + } +} diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index df3cfee171..ccfa09bbc1 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -92,6 +92,8 @@ export const moderationLogTypes = [ 'updateUserAnnouncement', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', + 'resetReadsForGlobalAnnouncement', + 'resetReadsForUserAnnouncement', 'resetPassword', 'suspendRemoteInstance', 'unsuspendRemoteInstance', @@ -235,6 +237,17 @@ export type ModerationLogPayloads = { userUsername: string; userHost: string | null; }; + resetReadsForGlobalAnnouncement: { + announcementId: string; + announcement: any; + }; + resetReadsForUserAnnouncement: { + announcementId: string; + announcement: any; + userId: string; + userUsername: string; + userHost: string | null; + }; resetPassword: { userId: string; userUsername: string; diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index fe499fabbf..2b687372d6 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -39,7 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.needConfirmationToRead }} - {{ i18n.ts.delete }} +
+ {{ i18n.ts.resetReads }} + {{ i18n.ts.delete }} +
@@ -117,6 +120,20 @@ async function done() { } } +async function resetReads() { + if (!props.announcement) return; + + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.tsx.resetReadsAreYouSure({ x: props.announcement.title }), + }); + if (canceled) return; + + await os.apiWithDialog('admin/announcements/reset-reads', { + announcementId: props.announcement.id, + }); +} + async function del() { const { canceled } = await os.confirm({ type: 'warning', diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 30d7e38638..29fad8c9d6 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -151,7 +151,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.new }} + {{ i18n.ts.add }} diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index e420586017..48e5aeaf5e 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -34,6 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.save }} {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }}) {{ i18n.ts.unarchive }} + {{ i18n.ts.resetReads }} {{ i18n.ts.delete }}
@@ -71,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.needConfirmationToRead }} -

{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}

+

{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}

@@ -85,6 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only