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
This commit is contained in:
syuilo
2023-08-13 20:12:29 +09:00
committed by GitHub
parent 2896fc6cb4
commit 9487856495
38 changed files with 1226 additions and 223 deletions

View File

@@ -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),
}));
}
}

View File

@@ -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,

View File

@@ -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');
}
@@ -208,19 +207,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> {
/*
@@ -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),