summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-08-13 20:12:29 +0900
committerGitHub <noreply@github.com>2023-08-13 20:12:29 +0900
commit948785649540e08c1610b1dcce6b37e99b5e8039 (patch)
treef5e57a0ecca79c8fb244b4c6af53be8e4b7a53fd /packages/backend/src
parentfix(frontend/MkUrlPreview): allow fullscreen from tweets (#11712) (diff)
downloadmisskey-948785649540e08c1610b1dcce6b37e99b5e8039.tar.gz
misskey-948785649540e08c1610b1dcce6b37e99b5e8039.tar.bz2
misskey-948785649540e08c1610b1dcce6b37e99b5e8039.zip
feat: refine announcement (#11497)
* wip * Update read-announcement.ts * wip * wip * wip * Update index.d.ts * wip * Create 1691649257651-refine-announcement.js * wip * wip * wip * wip * wip * wip * Update announcements.vue * wip * wip * Update announcements.vue * wip * Update announcements.vue * wip * Update misskey-js.api.md * Update users.ts * Create MkAnnouncementDialog.stories.impl.ts * wip * wip * Create AnnouncementService.ts
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/core/AnnouncementService.ts135
-rw-r--r--packages/backend/src/core/CoreModule.ts6
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts26
-rw-r--r--packages/backend/src/misc/json-schema.ts2
-rw-r--r--packages/backend/src/models/entities/Announcement.ts49
-rw-r--r--packages/backend/src/models/json-schema/announcement.ts58
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/create.ts28
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/list.ts12
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/update.ts12
-rw-r--r--packages/backend/src/server/api/endpoints/announcements.ts63
-rw-r--r--packages/backend/src/server/api/endpoints/i/read-announcement.ts55
-rw-r--r--packages/backend/src/server/api/stream/types.ts6
12 files changed, 317 insertions, 135 deletions
diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts
new file mode 100644
index 0000000000..482aeee39f
--- /dev/null
+++ b/packages/backend/src/core/AnnouncementService.ts
@@ -0,0 +1,135 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Brackets } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { User } from '@/models/entities/User.js';
+import type { AnnouncementReadsRepository, AnnouncementsRepository, Announcement, AnnouncementRead } from '@/models/index.js';
+import { bindThis } from '@/decorators.js';
+import { Packed } from '@/misc/json-schema.js';
+import { IdService } from '@/core/IdService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+
+@Injectable()
+export class AnnouncementService {
+ constructor(
+ @Inject(DI.announcementsRepository)
+ private announcementsRepository: AnnouncementsRepository,
+
+ @Inject(DI.announcementReadsRepository)
+ private announcementReadsRepository: AnnouncementReadsRepository,
+
+ private idService: IdService,
+ private globalEventService: GlobalEventService,
+ ) {
+ }
+
+ @bindThis
+ public async getReads(userId: User['id']): Promise<AnnouncementRead[]> {
+ return this.announcementReadsRepository.findBy({
+ userId: userId,
+ });
+ }
+
+ @bindThis
+ public async getUnreadAnnouncements(user: User): Promise<Announcement[]> {
+ const readsQuery = this.announcementReadsRepository.createQueryBuilder('read')
+ .select('read.announcementId')
+ .where('read.userId = :userId', { userId: user.id });
+
+ const q = this.announcementsRepository.createQueryBuilder('announcement')
+ .where('announcement.isActive = true')
+ .andWhere(new Brackets(qb => {
+ qb.orWhere('announcement.userId = :userId', { userId: user.id });
+ qb.orWhere('announcement.userId IS NULL');
+ }))
+ .andWhere(new Brackets(qb => {
+ qb.orWhere('announcement.forExistingUsers = false');
+ qb.orWhere('announcement.createdAt > :createdAt', { createdAt: user.createdAt });
+ }))
+ .andWhere(`announcement.id NOT IN (${ readsQuery.getQuery() })`);
+
+ q.setParameters(readsQuery.getParameters());
+
+ return q.getMany();
+ }
+
+ @bindThis
+ public async create(values: Partial<Announcement>): Promise<{ raw: Announcement; packed: Packed<'Announcement'> }> {
+ const announcement = await this.announcementsRepository.insert({
+ id: this.idService.genId(),
+ createdAt: new Date(),
+ updatedAt: null,
+ title: values.title,
+ text: values.text,
+ imageUrl: values.imageUrl,
+ icon: values.icon,
+ display: values.display,
+ forExistingUsers: values.forExistingUsers,
+ needConfirmationToRead: values.needConfirmationToRead,
+ userId: values.userId,
+ }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
+
+ const packed = (await this.packMany([announcement]))[0];
+
+ if (values.userId) {
+ this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
+ announcement: packed,
+ });
+ } else {
+ this.globalEventService.publishBroadcastStream('announcementCreated', {
+ announcement: packed,
+ });
+ }
+
+ return {
+ raw: announcement,
+ packed: packed,
+ };
+ }
+
+ @bindThis
+ public async read(user: User, announcementId: Announcement['id']): Promise<void> {
+ try {
+ await this.announcementReadsRepository.insert({
+ id: this.idService.genId(),
+ createdAt: new Date(),
+ announcementId: announcementId,
+ userId: user.id,
+ });
+ } catch (e) {
+ return;
+ }
+
+ if ((await this.getUnreadAnnouncements(user)).length === 0) {
+ this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
+ }
+ }
+
+ @bindThis
+ public async packMany(
+ announcements: Announcement[],
+ me?: { id: User['id'] } | null | undefined,
+ options?: {
+ reads?: AnnouncementRead[];
+ },
+ ): Promise<Packed<'Announcement'>[]> {
+ const reads = me ? (options?.reads ?? await this.getReads(me.id)) : [];
+ return announcements.map(announcement => ({
+ id: announcement.id,
+ createdAt: announcement.createdAt.toISOString(),
+ updatedAt: announcement.updatedAt?.toISOString() ?? null,
+ text: announcement.text,
+ title: announcement.title,
+ imageUrl: announcement.imageUrl,
+ icon: announcement.icon,
+ display: announcement.display,
+ needConfirmationToRead: announcement.needConfirmationToRead,
+ forYou: announcement.userId === me?.id,
+ isRead: reads.some(read => read.announcementId === announcement.id),
+ }));
+ }
+}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 9d6fb87923..51d4f9cfa9 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -7,6 +7,7 @@ import { Module } from '@nestjs/common';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
+import { AnnouncementService } from './AnnouncementService.js';
import { AntennaService } from './AntennaService.js';
import { AppLockService } from './AppLockService.js';
import { AchievementService } from './AchievementService.js';
@@ -130,6 +131,7 @@ const $LoggerService: Provider = { provide: 'LoggerService', useExisting: Logger
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
+const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService };
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
@@ -257,6 +259,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AccountMoveService,
AccountUpdateService,
AiService,
+ AnnouncementService,
AntennaService,
AppLockService,
AchievementService,
@@ -377,6 +380,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AccountMoveService,
$AccountUpdateService,
$AiService,
+ $AnnouncementService,
$AntennaService,
$AppLockService,
$AchievementService,
@@ -498,6 +502,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AccountMoveService,
AccountUpdateService,
AiService,
+ AnnouncementService,
AntennaService,
AppLockService,
AchievementService,
@@ -617,6 +622,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$AccountMoveService,
$AccountUpdateService,
$AiService,
+ $AnnouncementService,
$AntennaService,
$AppLockService,
$AchievementService,
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 1959224227..7be547fbb9 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -4,7 +4,6 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import { In, Not } from 'typeorm';
import * as Redis from 'ioredis';
import _Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
@@ -16,13 +15,13 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
-import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, UserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/index.js';
+import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, UserProfile, RenoteMutingsRepository, UserMemoRepository, Announcement } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import type { OnModuleInit } from '@nestjs/common';
-import type { AntennaService } from '../AntennaService.js';
+import type { AnnouncementService } from '../AnnouncementService.js';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
@@ -58,7 +57,7 @@ export class UserEntityService implements OnModuleInit {
private driveFileEntityService: DriveFileEntityService;
private pageEntityService: PageEntityService;
private customEmojiService: CustomEmojiService;
- private antennaService: AntennaService;
+ private announcementService: AnnouncementService;
private roleService: RoleService;
private federatedInstanceService: FederatedInstanceService;
@@ -128,7 +127,7 @@ export class UserEntityService implements OnModuleInit {
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.pageEntityService = this.moduleRef.get('PageEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
- this.antennaService = this.moduleRef.get('AntennaService');
+ this.announcementService = this.moduleRef.get('AnnouncementService');
this.roleService = this.moduleRef.get('RoleService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
}
@@ -209,19 +208,6 @@ export class UserEntityService implements OnModuleInit {
}
@bindThis
- public async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
- const reads = await this.announcementReadsRepository.findBy({
- userId: userId,
- });
-
- const count = await this.announcementsRepository.countBy(reads.length > 0 ? {
- id: Not(In(reads.map(read => read.announcementId))),
- } : {});
-
- return count > 0;
- }
-
- @bindThis
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
/*
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
@@ -347,6 +333,7 @@ export class UserEntityService implements OnModuleInit {
const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null;
+ const unreadAnnouncements = isMe && opts.detail ? await this.announcementService.getUnreadAnnouncements(user) : null;
const falsy = opts.detail ? false : undefined;
@@ -456,7 +443,8 @@ export class UserEntityService implements OnModuleInit {
where: { userId: user.id, isMentioned: true },
take: 1,
}).then(count => count > 0),
- hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
+ hasUnreadAnnouncement: unreadAnnouncements!.length > 0,
+ unreadAnnouncements,
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
hasUnreadChannel: false, // 後方互換性のため
hasUnreadNotification: this.getHasUnreadNotification(user.id),
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index ca8a030a62..80c1041c62 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -35,6 +35,7 @@ import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js';
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
import { packedFlashSchema } from '@/models/json-schema/flash.js';
+import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
export const refs = {
UserLite: packedUserLiteSchema,
@@ -46,6 +47,7 @@ export const refs = {
User: packedUserSchema,
UserList: packedUserListSchema,
+ Announcement: packedAnnouncementSchema,
App: packedAppSchema,
Note: packedNoteSchema,
NoteReaction: packedNoteReactionSchema,
diff --git a/packages/backend/src/models/entities/Announcement.ts b/packages/backend/src/models/entities/Announcement.ts
index 99cdf89330..18c26faab0 100644
--- a/packages/backend/src/models/entities/Announcement.ts
+++ b/packages/backend/src/models/entities/Announcement.ts
@@ -3,8 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
+import { Entity, Index, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
import { id } from '../id.js';
+import { User } from './User.js';
@Entity()
export class Announcement {
@@ -38,6 +39,52 @@ export class Announcement {
})
public imageUrl: string | null;
+ // info, warning, error, success
+ @Column('varchar', {
+ length: 256, nullable: false,
+ default: 'info',
+ })
+ public icon: string;
+
+ // normal ... お知らせページ掲載
+ // banner ... お知らせページ掲載 + バナー表示
+ // dialog ... お知らせページ掲載 + ダイアログ表示
+ @Column('varchar', {
+ length: 256, nullable: false,
+ default: 'normal',
+ })
+ public display: string;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public needConfirmationToRead: boolean;
+
+ @Index()
+ @Column('boolean', {
+ default: true,
+ })
+ public isActive: boolean;
+
+ @Index()
+ @Column('boolean', {
+ default: false,
+ })
+ public forExistingUsers: boolean;
+
+ @Index()
+ @Column({
+ ...id(),
+ nullable: true,
+ })
+ public userId: User['id'] | null;
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public user: User | null;
+
constructor(data: Partial<Announcement>) {
if (data == null) return;
diff --git a/packages/backend/src/models/json-schema/announcement.ts b/packages/backend/src/models/json-schema/announcement.ts
new file mode 100644
index 0000000000..c7e24c7f29
--- /dev/null
+++ b/packages/backend/src/models/json-schema/announcement.ts
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const packedAnnouncementSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ example: 'xxxxxxxxxx',
+ },
+ createdAt: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'date-time',
+ },
+ updatedAt: {
+ type: 'string',
+ optional: false, nullable: true,
+ format: 'date-time',
+ },
+ text: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ title: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ imageUrl: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ icon: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ display: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ forYou: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ needConfirmationToRead: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ isRead: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
+ },
+} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
index 8addffc6a8..6c5520c2ef 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
@@ -3,11 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { AnnouncementsRepository } from '@/models/index.js';
-import { IdService } from '@/core/IdService.js';
-import { DI } from '@/di-symbols.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
export const meta = {
tags: ['admin'],
@@ -57,6 +55,11 @@ export const paramDef = {
title: { type: 'string', minLength: 1 },
text: { type: 'string', minLength: 1 },
imageUrl: { type: 'string', nullable: true, minLength: 1 },
+ icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'], default: 'info' },
+ display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' },
+ forExistingUsers: { type: 'boolean', default: false },
+ needConfirmationToRead: { type: 'boolean', default: false },
+ userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
},
required: ['title', 'text', 'imageUrl'],
} as const;
@@ -65,22 +68,23 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
- @Inject(DI.announcementsRepository)
- private announcementsRepository: AnnouncementsRepository,
-
- private idService: IdService,
+ private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
- const announcement = await this.announcementsRepository.insert({
- id: this.idService.genId(),
+ const { raw, packed } = await this.announcementService.create({
createdAt: new Date(),
updatedAt: null,
title: ps.title,
text: ps.text,
imageUrl: ps.imageUrl,
- }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
+ icon: ps.icon,
+ display: ps.display,
+ forExistingUsers: ps.forExistingUsers,
+ needConfirmationToRead: ps.needConfirmationToRead,
+ userId: ps.userId,
+ });
- return Object.assign({}, announcement, { createdAt: announcement.createdAt.toISOString(), updatedAt: null });
+ return packed;
});
}
}
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 ec0d4061a6..4da3f457f9 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
@@ -66,6 +66,7 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
+ userId: { type: 'string', format: 'misskey:id', nullable: true },
},
required: [],
} as const;
@@ -84,6 +85,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
+ if (ps.userId) {
+ query.andWhere('announcement.userId = :userId', { userId: ps.userId });
+ } else {
+ query.andWhere('announcement.userId IS NULL');
+ }
const announcements = await query.limit(ps.limit).getMany();
@@ -102,6 +108,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
title: announcement.title,
text: announcement.text,
imageUrl: announcement.imageUrl,
+ icon: announcement.icon,
+ display: announcement.display,
+ isActive: announcement.isActive,
+ forExistingUsers: announcement.forExistingUsers,
+ needConfirmationToRead: announcement.needConfirmationToRead,
+ userId: announcement.userId,
reads: reads.get(announcement)!,
}));
});
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
index b3df14320f..7efc7c0402 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
@@ -31,8 +31,13 @@ export const paramDef = {
title: { type: 'string', minLength: 1 },
text: { type: 'string', minLength: 1 },
imageUrl: { type: 'string', nullable: true, minLength: 0 },
+ icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'] },
+ display: { type: 'string', enum: ['normal', 'banner', 'dialog'] },
+ forExistingUsers: { type: 'boolean' },
+ needConfirmationToRead: { type: 'boolean' },
+ isActive: { type: 'boolean' },
},
- required: ['id', 'title', 'text', 'imageUrl'],
+ required: ['id'],
} as const;
// eslint-disable-next-line import/no-default-export
@@ -53,6 +58,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
text: ps.text,
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
imageUrl: ps.imageUrl || null,
+ display: ps.display,
+ icon: ps.icon,
+ forExistingUsers: ps.forExistingUsers,
+ needConfirmationToRead: ps.needConfirmationToRead,
+ isActive: ps.isActive,
});
});
}
diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts
index df0a0afb09..070e6f0d77 100644
--- a/packages/backend/src/server/api/endpoints/announcements.ts
+++ b/packages/backend/src/server/api/endpoints/announcements.ts
@@ -4,8 +4,10 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
import { DI } from '@/di-symbols.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js';
@@ -20,40 +22,7 @@ export const meta = {
items: {
type: 'object',
optional: false, nullable: false,
- properties: {
- id: {
- type: 'string',
- optional: false, nullable: false,
- format: 'id',
- example: 'xxxxxxxxxx',
- },
- createdAt: {
- type: 'string',
- optional: false, nullable: false,
- format: 'date-time',
- },
- updatedAt: {
- type: 'string',
- optional: false, nullable: true,
- format: 'date-time',
- },
- text: {
- type: 'string',
- optional: false, nullable: false,
- },
- title: {
- type: 'string',
- optional: false, nullable: false,
- },
- imageUrl: {
- type: 'string',
- optional: false, nullable: true,
- },
- isRead: {
- type: 'boolean',
- optional: true, nullable: false,
- },
- },
+ ref: 'Announcement',
},
},
} as const;
@@ -62,9 +31,9 @@ export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
- withUnreads: { type: 'boolean', default: false },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
+ isActive: { type: 'boolean', default: true },
},
required: [],
} as const;
@@ -80,27 +49,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private announcementReadsRepository: AnnouncementReadsRepository,
private queryService: QueryService,
+ private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
+ const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId)
+ .where('announcement.isActive = :isActive', { isActive: ps.isActive })
+ .andWhere(new Brackets(qb => {
+ if (me) qb.orWhere('announcement.userId = :meId', { meId: me.id });
+ qb.orWhere('announcement.userId IS NULL');
+ }));
const announcements = await query.limit(ps.limit).getMany();
- if (me) {
- const reads = (await this.announcementReadsRepository.findBy({
- userId: me.id,
- })).map(x => x.announcementId);
-
- for (const announcement of announcements) {
- (announcement as any).isRead = reads.includes(announcement.id);
- }
- }
-
- return (ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements).map((a) => ({
- ...a,
- createdAt: a.createdAt.toISOString(),
- updatedAt: a.updatedAt?.toISOString() ?? null,
- }));
+ return this.announcementService.packMany(announcements, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts
index 7bda6e2627..412532939c 100644
--- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts
+++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts
@@ -3,14 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { IdService } from '@/core/IdService.js';
-import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { DI } from '@/di-symbols.js';
-import { ApiError } from '../../error.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
export const meta = {
tags: ['account'],
@@ -20,11 +15,6 @@ export const meta = {
kind: 'write:account',
errors: {
- noSuchAnnouncement: {
- message: 'No such announcement.',
- code: 'NO_SUCH_ANNOUNCEMENT',
- id: '184663db-df88-4bc2-8b52-fb85f0681939',
- },
},
} as const;
@@ -40,47 +30,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
- @Inject(DI.announcementsRepository)
- private announcementsRepository: AnnouncementsRepository,
-
- @Inject(DI.announcementReadsRepository)
- private announcementReadsRepository: AnnouncementReadsRepository,
-
- private userEntityService: UserEntityService,
- private idService: IdService,
- private globalEventService: GlobalEventService,
+ private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
- // Check if announcement exists
- const announcementExist = await this.announcementsRepository.exist({ where: { id: ps.announcementId } });
-
- if (!announcementExist) {
- throw new ApiError(meta.errors.noSuchAnnouncement);
- }
-
- // Check if already read
- const alreadyRead = await this.announcementReadsRepository.exist({
- where: {
- announcementId: ps.announcementId,
- userId: me.id,
- },
- });
-
- if (alreadyRead) {
- return;
- }
-
- // Create read
- await this.announcementReadsRepository.insert({
- id: this.idService.genId(),
- createdAt: new Date(),
- announcementId: ps.announcementId,
- userId: me.id,
- });
-
- if (!await this.userEntityService.getHasUnreadAnnouncement(me.id)) {
- this.globalEventService.publishMainStream(me.id, 'readAllAnnouncements');
- }
+ await this.announcementService.read(me, ps.announcementId);
});
}
}
diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts
index 82ccd91c88..751a23de8d 100644
--- a/packages/backend/src/server/api/stream/types.ts
+++ b/packages/backend/src/server/api/stream/types.ts
@@ -64,6 +64,9 @@ export interface BroadcastTypes {
[other: string]: any;
}[];
};
+ announcementCreated: {
+ announcement: Packed<'Announcement'>;
+ };
}
export interface MainStreamTypes {
@@ -105,6 +108,9 @@ export interface MainStreamTypes {
driveFileCreated: Packed<'DriveFile'>;
readAntenna: Antenna;
receiveFollowRequest: Packed<'User'>;
+ announcementCreated: {
+ announcement: Packed<'Announcement'>;
+ };
}
export interface DriveStreamTypes {