diff --git a/CHANGELOG.md b/CHANGELOG.md index d3a0e3195f..2e96ea5a96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - したがって、それらの種別不明ファイルを許可したい場合は application/octet-stream を指定に追加してください。 - Feat: プレビュー先がリダイレクトを伴う場合、リダイレクト先のコンテンツを取得しに行くか否かを設定できるように(#16043) - Enhance: UIのアイコンデータの読み込みを軽量化 +- Enhance: お知らせの既読をリセットできるように ### Client - Feat: ドライブのUIが強化されました diff --git a/locales/index.d.ts b/locales/index.d.ts index 73bcb2f1c8..585e221bc3 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5481,6 +5481,14 @@ export interface Locale extends ILocale { * 全ての「ヒントとコツ」を非表示 */ "hideAllTips": string; + /** + * 既読をリセット + */ + "resetReads": string; + /** + * 「{x}」の既読をリセットしますか? + */ + "resetReadsAreYouSure": ParameterizedString<"x">; "_chat": { /** * まだメッセージはありません @@ -10702,6 +10710,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 c7971507aa..188a0522f0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1365,6 +1365,8 @@ abort: "中止" tip: "ヒントとコツ" redisplayAllTips: "全ての「ヒントとコツ」を再表示" hideAllTips: "全ての「ヒントとコツ」を非表示" +resetReads: "既読をリセット" +resetReadsAreYouSure: "「{x}」の既読をリセットしますか?" _chat: noMessagesYet: "まだメッセージはありません" @@ -2835,6 +2837,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/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 1fdd000fdf..60c3eefffd 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -26,6 +26,7 @@ export * as 'admin/ad/update' from './endpoints/admin/ad/update.js'; export * as 'admin/announcements/create' from './endpoints/admin/announcements/create.js'; export * as 'admin/announcements/delete' from './endpoints/admin/announcements/delete.js'; export * as 'admin/announcements/list' from './endpoints/admin/announcements/list.js'; +export * as 'admin/announcements/reset-reads' from './endpoints/admin/announcements/reset-reads.js'; export * as 'admin/announcements/update' from './endpoints/admin/announcements/update.js'; export * as 'admin/avatar-decorations/create' from './endpoints/admin/avatar-decorations/create.js'; export * as 'admin/avatar-decorations/delete' from './endpoints/admin/avatar-decorations/delete.js'; 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 5d5f1e3b71..646e6c10ee 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -96,6 +96,8 @@ export const moderationLogTypes = [ 'updateUserAnnouncement', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', + 'resetReadsForGlobalAnnouncement', + 'resetReadsForUserAnnouncement', 'resetPassword', 'suspendRemoteInstance', 'unsuspendRemoteInstance', @@ -241,6 +243,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 8ec48dcc3f..5d425d25b3 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 15cd219834..c1e97a848d 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -152,7 +152,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 b2d7b4889a..31e30abf7b 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -33,6 +33,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 }}
@@ -70,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.needConfirmationToRead }} -

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

+

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

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