diff --git a/locales/en-US.yml b/locales/en-US.yml index 234ab73a6..49b0fde36 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -251,7 +251,6 @@ noSuchUser: "User not found" lookup: "Lookup" announcements: "Announcements" imageUrl: "Image URL" -displayOrder: "Position" remove: "Delete" removed: "Successfully deleted" removeAreYouSure: "Are you sure that you want to remove \"{x}\"?" diff --git a/locales/index.d.ts b/locales/index.d.ts index e97eec6c6..e7f5a821b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -254,7 +254,6 @@ export interface Locale { "lookup": string; "announcements": string; "imageUrl": string; - "displayOrder": string; "remove": string; "removed": string; "removeAreYouSure": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a4d877954..a5ec47546 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -251,7 +251,6 @@ noSuchUser: "ユーザーが見つかりません" lookup: "照会" announcements: "お知らせ" imageUrl: "画像URL" -displayOrder: "表示順" remove: "削除" removed: "削除しました" removeAreYouSure: "「{x}」を削除しますか?" diff --git a/packages/backend/migration/1690463372775-announcement-display-order.js b/packages/backend/migration/1690463372775-announcement-display-order.js deleted file mode 100644 index 81baa5589..000000000 --- a/packages/backend/migration/1690463372775-announcement-display-order.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class AnnouncementDisplayOrder1690463372775 { - name = 'AnnouncementDisplayOrder1690463372775' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "announcement" ADD "displayOrder" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`CREATE INDEX "IDX_b64d293ca4bef21e91963054b0" ON "announcement" ("displayOrder") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_b64d293ca4bef21e91963054b0"`); - await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "displayOrder"`); - } -} diff --git a/packages/backend/src/models/entities/Announcement.ts b/packages/backend/src/models/entities/Announcement.ts index 4929a791f..7a782b1e1 100644 --- a/packages/backend/src/models/entities/Announcement.ts +++ b/packages/backend/src/models/entities/Announcement.ts @@ -38,13 +38,6 @@ export class Announcement { }) public imageUrl: string | null; - // UIに表示する際の並び順用(大きいほど先頭) - @Index() - @Column('integer', { - default: 0, - }) - public displayOrder: number; - @Index() @Column('varchar', { ...id(), 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 38f69a892..246efcf14 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -47,10 +47,6 @@ export const meta = { type: 'string', optional: false, nullable: true, }, - displayOrder: { - type: 'number', - optional: false, nullable: false, - }, userId: { type: 'string', optional: false, nullable: true, @@ -69,7 +65,6 @@ export const paramDef = { title: { type: 'string', minLength: 1 }, text: { type: 'string', minLength: 1 }, imageUrl: { type: 'string', nullable: true, minLength: 1 }, - displayOrder: { type: 'number' }, userId: { type: 'string', nullable: true, format: 'misskey:id' }, closeDuration: { type: 'number', nullable: false }, }, @@ -93,7 +88,6 @@ export default class extends Endpoint { title: ps.title, text: ps.text, imageUrl: ps.imageUrl, - displayOrder: ps.displayOrder, userId: ps.userId ?? null, closeDuration: ps.closeDuration, }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0])); 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 a0d554d94..288b4e665 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -53,10 +53,6 @@ export const meta = { type: 'string', optional: false, nullable: true, }, - displayOrder: { - type: 'number', - optional: false, nullable: false, - }, userId: { type: 'string', optional: false, nullable: true, @@ -83,7 +79,8 @@ export const paramDef = { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - offset: { type: 'integer', default: 0 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' }, }, required: [], @@ -102,25 +99,20 @@ export default class extends Endpoint { @Inject(DI.usersRepository) private usersRepository: UsersRepository, + private queryService: QueryService, private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.announcementsRepository.createQueryBuilder('announcement'); + const builder = this.announcementsRepository.createQueryBuilder('announcement'); if (ps.userId) { - query.where('"userId" = :userId', { userId: ps.userId }); + builder.where('"userId" = :userId', { userId: ps.userId }); } else { - query.where('"userId" IS NULL'); + builder.where('"userId" IS NULL'); } - query.orderBy({ - 'announcement."displayOrder"': 'DESC', - 'announcement."createdAt"': 'DESC', - }); + const query = this.queryService.makePaginationQuery(builder, ps.sinceId, ps.untilId); - const announcements = await query - .offset(ps.offset) - .limit(ps.limit) - .getMany(); + const announcements = await query.limit(ps.limit).getMany(); const reads = new Map(); @@ -144,7 +136,6 @@ export default class extends Endpoint { title: announcement.title, text: announcement.text, imageUrl: announcement.imageUrl, - displayOrder: announcement.displayOrder, userId: announcement.userId, user: packedUsers.find(user => user.id === 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 9290b1370..2e18f1966 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -31,7 +31,6 @@ export const paramDef = { title: { type: 'string', minLength: 1 }, text: { type: 'string', minLength: 1 }, imageUrl: { type: 'string', nullable: true, minLength: 0 }, - displayOrder: { type: 'number' }, userId: { type: 'string', nullable: true, format: 'misskey:id' }, closeDuration: { type: 'number', nullable: false }, }, @@ -63,7 +62,6 @@ export default class extends Endpoint { text: ps.text, /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ imageUrl: ps.imageUrl || null, - displayOrder: ps.displayOrder, userId: ps.userId ?? null, closeDuration: ps.closeDuration, }); diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index d1a71ffe3..b951c2bce 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -5,9 +5,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; -import type { AnnouncementsRepository } from '@/models/index.js'; -import { Announcement, AnnouncementRead } from '@/models/index.js'; +import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js'; export const meta = { tags: ['meta'], @@ -70,8 +70,9 @@ export const paramDef = { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - offset: { type: 'integer', default: 0 }, withUnreads: { type: 'boolean', default: false }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, privateOnly: { type: 'boolean', default: false }, }, required: [], @@ -83,37 +84,39 @@ export default class extends Endpoint { constructor( @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, + + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, + + private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.announcementsRepository.createQueryBuilder('announcement'); + const builder = this.announcementsRepository.createQueryBuilder('announcement'); if (me) { - query.leftJoinAndSelect(AnnouncementRead, 'reads', 'reads."announcementId" = announcement.id AND reads."userId" = :userId', { userId: me.id }); - query.select([ - 'announcement.*', - 'CASE WHEN reads.id IS NULL THEN FALSE ELSE TRUE END as "isRead"', - ]); if (ps.privateOnly) { - query.where('announcement."userId" = :userId', { userId: me.id }); + builder.where('"userId" = :userId', { userId: me.id }); } else { - query.where('announcement."userId" IS NULL'); - query.orWhere('announcement."userId" = :userId', { userId: me.id }); + builder.where('"userId" IS NULL'); + builder.orWhere('"userId" = :userId', { userId: me.id }); } } else { - query.where('announcement."userId" IS NULL'); + builder.where('"userId" IS NULL'); } - query.orderBy({ - '"isRead"': 'ASC', - 'announcement."displayOrder"': 'DESC', - 'announcement."createdAt"': 'DESC', - }); + const query = this.queryService.makePaginationQuery(builder, ps.sinceId, ps.untilId); + const announcements = await query.limit(ps.limit).getMany(); - const announcements = await query - .offset(ps.offset) - .limit(ps.limit) - .getRawMany(); + if (me) { + const reads = (await this.announcementReadsRepository.findBy({ + userId: me.id, + })).map(x => x.announcementId); - return (ps.withUnreads ? announcements.filter(i => !i.isRead) : announcements).map((a) => ({ + 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, diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index a713048f1..7e493f5c6 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
@@ -35,9 +35,6 @@ SPDX-License-Identifier: AGPL-3.0-only - - - @@ -51,7 +48,6 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.loadMore }} @@ -70,31 +66,33 @@ import * as os from '@/os'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; -const announceTitleEl = $shallowRef(null); -const user = ref(null); -const offset = ref(0); -const hasMore = ref(false); - let announcements: any[] = $ref([]); -function insertEmoji(ev: MouseEvent): void { - os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, announceTitleEl); -} +const user = ref(null); +const announceTitleEl = $shallowRef(null); -function selectUserFilter(): void { +function selectUserFilter() { os.selectUser().then(_user => { user.value = _user; }); } -function editUser(announcement): void { +function editUser(an) { os.selectUser().then(_user => { - announcement.userId = _user.id; - announcement.user = _user; + an.userId = _user.id; + an.user = _user; }); } -function add(): void { +async function insertEmoji(ev: MouseEvent) { + os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, announceTitleEl); +} + +os.api('admin/announcements/list').then(announcementResponse => { + announcements = announcementResponse; +}); + +function add() { announcements.unshift({ id: null, title: '', @@ -102,12 +100,11 @@ function add(): void { imageUrl: null, userId: null, user: null, - displayOrder: 0, closeDuration: 10, }); } -function remove(announcement): void { +function remove(announcement) { os.confirm({ type: 'warning', text: i18n.t('removeAreYouSure', { x: announcement.title }), @@ -118,14 +115,14 @@ function remove(announcement): void { }); } -function save(announcement): void { +function save(announcement) { if (announcement.id == null) { os.api('admin/announcements/create', announcement).then(() => { os.alert({ type: 'success', text: i18n.ts.saved, }); - fetch(true); + refresh(); }).catch(err => { os.alert({ type: 'error', @@ -147,26 +144,15 @@ function save(announcement): void { } } -function fetch(resetOffset = false): void { - if (resetOffset) { - announcements = []; - offset.value = 0; - } - - os.api('admin/announcements/list', { - offsetMode: true, - offset: offset.value, - limit: 10, - userId: user.value?.id, - }).then(announcementResponse => { - announcements = announcements.concat(announcementResponse); - hasMore.value = announcementResponse?.length === 10; - offset.value += announcements.length; +function refresh() { + os.api('admin/announcements/list', { userId: user.value?.id }).then(announcementResponse => { + announcements = announcementResponse; }); } -watch(user, () => fetch(true)); -fetch(); +watch(user, refresh); + +refresh(); const headerActions = $computed(() => [{ asFullButton: true, @@ -182,10 +168,3 @@ definePageMetadata({ icon: 'ti ti-speakerphone', }); - - diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index f5887faf8..a82d1da4c 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only