@@ -251,7 +251,6 @@ noSuchUser: "User not found"
 | 
				
			|||||||
lookup: "Lookup"
 | 
					lookup: "Lookup"
 | 
				
			||||||
announcements: "Announcements"
 | 
					announcements: "Announcements"
 | 
				
			||||||
imageUrl: "Image URL"
 | 
					imageUrl: "Image URL"
 | 
				
			||||||
displayOrder: "Position"
 | 
					 | 
				
			||||||
remove: "Delete"
 | 
					remove: "Delete"
 | 
				
			||||||
removed: "Successfully deleted"
 | 
					removed: "Successfully deleted"
 | 
				
			||||||
removeAreYouSure: "Are you sure that you want to remove \"{x}\"?"
 | 
					removeAreYouSure: "Are you sure that you want to remove \"{x}\"?"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -254,7 +254,6 @@ export interface Locale {
 | 
				
			|||||||
    "lookup": string;
 | 
					    "lookup": string;
 | 
				
			||||||
    "announcements": string;
 | 
					    "announcements": string;
 | 
				
			||||||
    "imageUrl": string;
 | 
					    "imageUrl": string;
 | 
				
			||||||
    "displayOrder": string;
 | 
					 | 
				
			||||||
    "remove": string;
 | 
					    "remove": string;
 | 
				
			||||||
    "removed": string;
 | 
					    "removed": string;
 | 
				
			||||||
    "removeAreYouSure": string;
 | 
					    "removeAreYouSure": string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -251,7 +251,6 @@ noSuchUser: "ユーザーが見つかりません"
 | 
				
			|||||||
lookup: "照会"
 | 
					lookup: "照会"
 | 
				
			||||||
announcements: "お知らせ"
 | 
					announcements: "お知らせ"
 | 
				
			||||||
imageUrl: "画像URL"
 | 
					imageUrl: "画像URL"
 | 
				
			||||||
displayOrder: "表示順"
 | 
					 | 
				
			||||||
remove: "削除"
 | 
					remove: "削除"
 | 
				
			||||||
removed: "削除しました"
 | 
					removed: "削除しました"
 | 
				
			||||||
removeAreYouSure: "「{x}」を削除しますか?"
 | 
					removeAreYouSure: "「{x}」を削除しますか?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -38,13 +38,6 @@ export class Announcement {
 | 
				
			|||||||
	})
 | 
						})
 | 
				
			||||||
	public imageUrl: string | null;
 | 
						public imageUrl: string | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// UIに表示する際の並び順用(大きいほど先頭)
 | 
					 | 
				
			||||||
	@Index()
 | 
					 | 
				
			||||||
	@Column('integer', {
 | 
					 | 
				
			||||||
		default: 0,
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	public displayOrder: number;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Index()
 | 
						@Index()
 | 
				
			||||||
	@Column('varchar', {
 | 
						@Column('varchar', {
 | 
				
			||||||
		...id(),
 | 
							...id(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,10 +47,6 @@ export const meta = {
 | 
				
			|||||||
				type: 'string',
 | 
									type: 'string',
 | 
				
			||||||
				optional: false, nullable: true,
 | 
									optional: false, nullable: true,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			displayOrder: {
 | 
					 | 
				
			||||||
				type: 'number',
 | 
					 | 
				
			||||||
				optional: false, nullable: false,
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
			userId: {
 | 
								userId: {
 | 
				
			||||||
				type: 'string',
 | 
									type: 'string',
 | 
				
			||||||
				optional: false, nullable: true,
 | 
									optional: false, nullable: true,
 | 
				
			||||||
@@ -69,7 +65,6 @@ export const paramDef = {
 | 
				
			|||||||
		title: { type: 'string', minLength: 1 },
 | 
							title: { type: 'string', minLength: 1 },
 | 
				
			||||||
		text: { type: 'string', minLength: 1 },
 | 
							text: { type: 'string', minLength: 1 },
 | 
				
			||||||
		imageUrl: { type: 'string', nullable: true, minLength: 1 },
 | 
							imageUrl: { type: 'string', nullable: true, minLength: 1 },
 | 
				
			||||||
		displayOrder: { type: 'number' },
 | 
					 | 
				
			||||||
		userId: { type: 'string', nullable: true, format: 'misskey:id' },
 | 
							userId: { type: 'string', nullable: true, format: 'misskey:id' },
 | 
				
			||||||
		closeDuration: { type: 'number', nullable: false },
 | 
							closeDuration: { type: 'number', nullable: false },
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
@@ -93,7 +88,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			|||||||
				title: ps.title,
 | 
									title: ps.title,
 | 
				
			||||||
				text: ps.text,
 | 
									text: ps.text,
 | 
				
			||||||
				imageUrl: ps.imageUrl,
 | 
									imageUrl: ps.imageUrl,
 | 
				
			||||||
				displayOrder: ps.displayOrder,
 | 
					 | 
				
			||||||
				userId: ps.userId ?? null,
 | 
									userId: ps.userId ?? null,
 | 
				
			||||||
				closeDuration: ps.closeDuration,
 | 
									closeDuration: ps.closeDuration,
 | 
				
			||||||
			}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
 | 
								}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,10 +53,6 @@ export const meta = {
 | 
				
			|||||||
					type: 'string',
 | 
										type: 'string',
 | 
				
			||||||
					optional: false, nullable: true,
 | 
										optional: false, nullable: true,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				displayOrder: {
 | 
					 | 
				
			||||||
					type: 'number',
 | 
					 | 
				
			||||||
					optional: false, nullable: false,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				userId: {
 | 
									userId: {
 | 
				
			||||||
					type: 'string',
 | 
										type: 'string',
 | 
				
			||||||
					optional: false, nullable: true,
 | 
										optional: false, nullable: true,
 | 
				
			||||||
@@ -83,7 +79,8 @@ export const paramDef = {
 | 
				
			|||||||
	type: 'object',
 | 
						type: 'object',
 | 
				
			||||||
	properties: {
 | 
						properties: {
 | 
				
			||||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
							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' },
 | 
							userId: { type: 'string', format: 'misskey:id' },
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	required: [],
 | 
						required: [],
 | 
				
			||||||
@@ -102,25 +99,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			|||||||
		@Inject(DI.usersRepository)
 | 
							@Inject(DI.usersRepository)
 | 
				
			||||||
		private usersRepository: UsersRepository,
 | 
							private usersRepository: UsersRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							private queryService: QueryService,
 | 
				
			||||||
		private userEntityService: UserEntityService,
 | 
							private userEntityService: UserEntityService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		super(meta, paramDef, async (ps, me) => {
 | 
							super(meta, paramDef, async (ps, me) => {
 | 
				
			||||||
			const query = this.announcementsRepository.createQueryBuilder('announcement');
 | 
								const builder = this.announcementsRepository.createQueryBuilder('announcement');
 | 
				
			||||||
			if (ps.userId) {
 | 
								if (ps.userId) {
 | 
				
			||||||
				query.where('"userId" = :userId', { userId: ps.userId });
 | 
									builder.where('"userId" = :userId', { userId: ps.userId });
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				query.where('"userId" IS NULL');
 | 
									builder.where('"userId" IS NULL');
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			query.orderBy({
 | 
								const query = this.queryService.makePaginationQuery(builder, ps.sinceId, ps.untilId);
 | 
				
			||||||
				'announcement."displayOrder"': 'DESC',
 | 
					 | 
				
			||||||
				'announcement."createdAt"': 'DESC',
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const announcements = await query
 | 
								const announcements = await query.limit(ps.limit).getMany();
 | 
				
			||||||
				.offset(ps.offset)
 | 
					 | 
				
			||||||
				.limit(ps.limit)
 | 
					 | 
				
			||||||
				.getMany();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const reads = new Map<Announcement, number>();
 | 
								const reads = new Map<Announcement, number>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -144,7 +136,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			|||||||
				title: announcement.title,
 | 
									title: announcement.title,
 | 
				
			||||||
				text: announcement.text,
 | 
									text: announcement.text,
 | 
				
			||||||
				imageUrl: announcement.imageUrl,
 | 
									imageUrl: announcement.imageUrl,
 | 
				
			||||||
				displayOrder: announcement.displayOrder,
 | 
					 | 
				
			||||||
				userId: announcement.userId,
 | 
									userId: announcement.userId,
 | 
				
			||||||
				user: packedUsers.find(user => user.id === announcement.userId),
 | 
									user: packedUsers.find(user => user.id === announcement.userId),
 | 
				
			||||||
				reads: reads.get(announcement)!,
 | 
									reads: reads.get(announcement)!,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,7 +31,6 @@ export const paramDef = {
 | 
				
			|||||||
		title: { type: 'string', minLength: 1 },
 | 
							title: { type: 'string', minLength: 1 },
 | 
				
			||||||
		text: { type: 'string', minLength: 1 },
 | 
							text: { type: 'string', minLength: 1 },
 | 
				
			||||||
		imageUrl: { type: 'string', nullable: true, minLength: 0 },
 | 
							imageUrl: { type: 'string', nullable: true, minLength: 0 },
 | 
				
			||||||
		displayOrder: { type: 'number' },
 | 
					 | 
				
			||||||
		userId: { type: 'string', nullable: true, format: 'misskey:id' },
 | 
							userId: { type: 'string', nullable: true, format: 'misskey:id' },
 | 
				
			||||||
		closeDuration: { type: 'number', nullable: false },
 | 
							closeDuration: { type: 'number', nullable: false },
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
@@ -63,7 +62,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			|||||||
				text: ps.text,
 | 
									text: ps.text,
 | 
				
			||||||
				/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
 | 
									/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
 | 
				
			||||||
				imageUrl: ps.imageUrl || null,
 | 
									imageUrl: ps.imageUrl || null,
 | 
				
			||||||
				displayOrder: ps.displayOrder,
 | 
					 | 
				
			||||||
				userId: ps.userId ?? null,
 | 
									userId: ps.userId ?? null,
 | 
				
			||||||
				closeDuration: ps.closeDuration,
 | 
									closeDuration: ps.closeDuration,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,9 +5,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
					import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
				
			||||||
 | 
					import { QueryService } from '@/core/QueryService.js';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import type { AnnouncementsRepository } from '@/models/index.js';
 | 
					import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js';
 | 
				
			||||||
import { Announcement, AnnouncementRead } from '@/models/index.js';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const meta = {
 | 
					export const meta = {
 | 
				
			||||||
	tags: ['meta'],
 | 
						tags: ['meta'],
 | 
				
			||||||
@@ -70,8 +70,9 @@ export const paramDef = {
 | 
				
			|||||||
	type: 'object',
 | 
						type: 'object',
 | 
				
			||||||
	properties: {
 | 
						properties: {
 | 
				
			||||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
							limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
				
			||||||
		offset: { type: 'integer', default: 0 },
 | 
					 | 
				
			||||||
		withUnreads: { type: 'boolean', default: false },
 | 
							withUnreads: { type: 'boolean', default: false },
 | 
				
			||||||
 | 
							sinceId: { type: 'string', format: 'misskey:id' },
 | 
				
			||||||
 | 
							untilId: { type: 'string', format: 'misskey:id' },
 | 
				
			||||||
		privateOnly: { type: 'boolean', default: false },
 | 
							privateOnly: { type: 'boolean', default: false },
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	required: [],
 | 
						required: [],
 | 
				
			||||||
@@ -83,37 +84,39 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			|||||||
	constructor(
 | 
						constructor(
 | 
				
			||||||
		@Inject(DI.announcementsRepository)
 | 
							@Inject(DI.announcementsRepository)
 | 
				
			||||||
		private announcementsRepository: AnnouncementsRepository,
 | 
							private announcementsRepository: AnnouncementsRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@Inject(DI.announcementReadsRepository)
 | 
				
			||||||
 | 
							private announcementReadsRepository: AnnouncementReadsRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							private queryService: QueryService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		super(meta, paramDef, async (ps, me) => {
 | 
							super(meta, paramDef, async (ps, me) => {
 | 
				
			||||||
			const query = this.announcementsRepository.createQueryBuilder('announcement');
 | 
								const builder = this.announcementsRepository.createQueryBuilder('announcement');
 | 
				
			||||||
			if (me) {
 | 
								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) {
 | 
									if (ps.privateOnly) {
 | 
				
			||||||
					query.where('announcement."userId" = :userId', { userId: me.id });
 | 
										builder.where('"userId" = :userId', { userId: me.id });
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					query.where('announcement."userId" IS NULL');
 | 
										builder.where('"userId" IS NULL');
 | 
				
			||||||
					query.orWhere('announcement."userId" = :userId', { userId: me.id });
 | 
										builder.orWhere('"userId" = :userId', { userId: me.id });
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				query.where('announcement."userId" IS NULL');
 | 
									builder.where('"userId" IS NULL');
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			query.orderBy({
 | 
								const query = this.queryService.makePaginationQuery(builder, ps.sinceId, ps.untilId);
 | 
				
			||||||
				'"isRead"': 'ASC',
 | 
								const announcements = await query.limit(ps.limit).getMany();
 | 
				
			||||||
				'announcement."displayOrder"': 'DESC',
 | 
					 | 
				
			||||||
				'announcement."createdAt"': 'DESC',
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const announcements = await query
 | 
								if (me) {
 | 
				
			||||||
				.offset(ps.offset)
 | 
									const reads = (await this.announcementReadsRepository.findBy({
 | 
				
			||||||
				.limit(ps.limit)
 | 
										userId: me.id,
 | 
				
			||||||
				.getRawMany<Announcement & { isRead: boolean }>();
 | 
									})).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,
 | 
									...a,
 | 
				
			||||||
				createdAt: a.createdAt.toISOString(),
 | 
									createdAt: a.createdAt.toISOString(),
 | 
				
			||||||
				updatedAt: a.updatedAt?.toISOString() ?? null,
 | 
									updatedAt: a.updatedAt?.toISOString() ?? null,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</MkFolder>
 | 
									</MkFolder>
 | 
				
			||||||
			</MkFolder>
 | 
								</MkFolder>
 | 
				
			||||||
			<section v-for="announcement in announcements" :key="announcement.id">
 | 
								<section v-for="announcement in announcements" class="">
 | 
				
			||||||
				<div class="_panel _gaps_m" style="padding: 24px;">
 | 
									<div class="_panel _gaps_m" style="padding: 24px;">
 | 
				
			||||||
					<MkInput ref="announceTitleEl" v-model="announcement.title" :large="false">
 | 
										<MkInput ref="announceTitleEl" v-model="announcement.title" :large="false">
 | 
				
			||||||
						<template #label>{{ i18n.ts.title }} <button v-tooltip="i18n.ts.emoji" :class="['_button']" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button></template>
 | 
											<template #label>{{ i18n.ts.title }} <button v-tooltip="i18n.ts.emoji" :class="['_button']" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button></template>
 | 
				
			||||||
@@ -35,9 +35,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
					<MkInput v-model="announcement.imageUrl">
 | 
										<MkInput v-model="announcement.imageUrl">
 | 
				
			||||||
						<template #label>{{ i18n.ts.imageUrl }}</template>
 | 
											<template #label>{{ i18n.ts.imageUrl }}</template>
 | 
				
			||||||
					</MkInput>
 | 
										</MkInput>
 | 
				
			||||||
					<MkInput v-model="announcement.displayOrder" type="number">
 | 
					 | 
				
			||||||
						<template #label>{{ i18n.ts.displayOrder }}</template>
 | 
					 | 
				
			||||||
					</MkInput>
 | 
					 | 
				
			||||||
					<MkInput v-model="announcement.closeDuration" type="number">
 | 
										<MkInput v-model="announcement.closeDuration" type="number">
 | 
				
			||||||
						<template #label>{{ i18n.ts.dialogCloseDuration }}</template>
 | 
											<template #label>{{ i18n.ts.dialogCloseDuration }}</template>
 | 
				
			||||||
						<template #suffix>{{ i18n.ts._time.second }}</template>
 | 
											<template #suffix>{{ i18n.ts._time.second }}</template>
 | 
				
			||||||
@@ -51,7 +48,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</section>
 | 
								</section>
 | 
				
			||||||
			<MkButton v-if="hasMore" :class="$style.more" :disabled="!hasMore" primary rounded @click="fetch()">{{ i18n.ts.loadMore }}</MkButton>
 | 
					 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</MkSpacer>
 | 
						</MkSpacer>
 | 
				
			||||||
</MkStickyContainer>
 | 
					</MkStickyContainer>
 | 
				
			||||||
@@ -70,31 +66,33 @@ import * as os from '@/os';
 | 
				
			|||||||
import { i18n } from '@/i18n';
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
import { definePageMetadata } from '@/scripts/page-metadata';
 | 
					import { definePageMetadata } from '@/scripts/page-metadata';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const announceTitleEl = $shallowRef<HTMLInputElement | null>(null);
 | 
					 | 
				
			||||||
const user = ref<UserLite | null>(null);
 | 
					 | 
				
			||||||
const offset = ref(0);
 | 
					 | 
				
			||||||
const hasMore = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
let announcements: any[] = $ref([]);
 | 
					let announcements: any[] = $ref([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function insertEmoji(ev: MouseEvent): void {
 | 
					const user = ref<UserLite>(null);
 | 
				
			||||||
	os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, announceTitleEl);
 | 
					const announceTitleEl = $shallowRef<HTMLInputElement | null>(null);
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function selectUserFilter(): void {
 | 
					function selectUserFilter() {
 | 
				
			||||||
	os.selectUser().then(_user => {
 | 
						os.selectUser().then(_user => {
 | 
				
			||||||
		user.value = _user;
 | 
							user.value = _user;
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function editUser(announcement): void {
 | 
					function editUser(an) {
 | 
				
			||||||
	os.selectUser().then(_user => {
 | 
						os.selectUser().then(_user => {
 | 
				
			||||||
		announcement.userId = _user.id;
 | 
							an.userId = _user.id;
 | 
				
			||||||
		announcement.user = _user;
 | 
							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({
 | 
						announcements.unshift({
 | 
				
			||||||
		id: null,
 | 
							id: null,
 | 
				
			||||||
		title: '',
 | 
							title: '',
 | 
				
			||||||
@@ -102,12 +100,11 @@ function add(): void {
 | 
				
			|||||||
		imageUrl: null,
 | 
							imageUrl: null,
 | 
				
			||||||
		userId: null,
 | 
							userId: null,
 | 
				
			||||||
		user: null,
 | 
							user: null,
 | 
				
			||||||
		displayOrder: 0,
 | 
					 | 
				
			||||||
		closeDuration: 10,
 | 
							closeDuration: 10,
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function remove(announcement): void {
 | 
					function remove(announcement) {
 | 
				
			||||||
	os.confirm({
 | 
						os.confirm({
 | 
				
			||||||
		type: 'warning',
 | 
							type: 'warning',
 | 
				
			||||||
		text: i18n.t('removeAreYouSure', { x: announcement.title }),
 | 
							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) {
 | 
						if (announcement.id == null) {
 | 
				
			||||||
		os.api('admin/announcements/create', announcement).then(() => {
 | 
							os.api('admin/announcements/create', announcement).then(() => {
 | 
				
			||||||
			os.alert({
 | 
								os.alert({
 | 
				
			||||||
				type: 'success',
 | 
									type: 'success',
 | 
				
			||||||
				text: i18n.ts.saved,
 | 
									text: i18n.ts.saved,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			fetch(true);
 | 
								refresh();
 | 
				
			||||||
		}).catch(err => {
 | 
							}).catch(err => {
 | 
				
			||||||
			os.alert({
 | 
								os.alert({
 | 
				
			||||||
				type: 'error',
 | 
									type: 'error',
 | 
				
			||||||
@@ -147,26 +144,15 @@ function save(announcement): void {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function fetch(resetOffset = false): void {
 | 
					function refresh() {
 | 
				
			||||||
	if (resetOffset) {
 | 
						os.api('admin/announcements/list', { userId: user.value?.id }).then(announcementResponse => {
 | 
				
			||||||
		announcements = [];
 | 
							announcements = announcementResponse;
 | 
				
			||||||
		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;
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
watch(user, () => fetch(true));
 | 
					watch(user, refresh);
 | 
				
			||||||
fetch();
 | 
					
 | 
				
			||||||
 | 
					refresh();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const headerActions = $computed(() => [{
 | 
					const headerActions = $computed(() => [{
 | 
				
			||||||
	asFullButton: true,
 | 
						asFullButton: true,
 | 
				
			||||||
@@ -182,10 +168,3 @@ definePageMetadata({
 | 
				
			|||||||
	icon: 'ti ti-speakerphone',
 | 
						icon: 'ti ti-speakerphone',
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="scss" module>
 | 
					 | 
				
			||||||
.more {
 | 
					 | 
				
			||||||
  margin-left: auto;
 | 
					 | 
				
			||||||
  margin-right: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
<MkStickyContainer>
 | 
					<MkStickyContainer>
 | 
				
			||||||
	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 | 
						<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 | 
				
			||||||
	<MkSpacer :contentMax="800">
 | 
						<MkSpacer :contentMax="800">
 | 
				
			||||||
		<MkPagination v-slot="{items, reload}" :pagination="pagination" class="ruryvtyk _gaps_m">
 | 
							<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _gaps_m">
 | 
				
			||||||
			<section v-for="(announcement, i) in items" :key="announcement.id" :class="{ announcement: true, _panel: true, private: announcement.isPrivate }">
 | 
								<section v-for="(announcement, i) in items" :key="announcement.id" :class="{ announcement: true, _panel: true, private: announcement.isPrivate }">
 | 
				
			||||||
				<div class="header"><span v-if="$i && !announcement.isRead"><span class="ti ti-speakerphone"></span></span><Mfm :text="announcement.title"/></div>
 | 
									<div class="header"><span v-if="$i && !announcement.isRead"><span class="ti ti-speakerphone"></span></span><Mfm :text="announcement.title"/></div>
 | 
				
			||||||
				<div class="content">
 | 
									<div class="content">
 | 
				
			||||||
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
					<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
 | 
										<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<div v-if="$i && !announcement.isRead" class="footer">
 | 
									<div v-if="$i && !announcement.isRead" class="footer">
 | 
				
			||||||
					<MkButton primary @click="read(items, reload, announcement, i)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
 | 
										<MkButton primary @click="read(items, announcement, i)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</section>
 | 
								</section>
 | 
				
			||||||
		</MkPagination>
 | 
							</MkPagination>
 | 
				
			||||||
@@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { } from 'vue';
 | 
				
			||||||
import MkPagination from '@/components/MkPagination.vue';
 | 
					import MkPagination from '@/components/MkPagination.vue';
 | 
				
			||||||
import MkButton from '@/components/MkButton.vue';
 | 
					import MkButton from '@/components/MkButton.vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
@@ -33,15 +34,16 @@ import { $i } from '@/account';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const pagination = {
 | 
					const pagination = {
 | 
				
			||||||
	endpoint: 'announcements' as const,
 | 
						endpoint: 'announcements' as const,
 | 
				
			||||||
	offsetMode: true,
 | 
					 | 
				
			||||||
	limit: 10,
 | 
						limit: 10,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function read(items, reload, announcement, i) {
 | 
					// TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい
 | 
				
			||||||
	items[i].isRead = true;
 | 
					function read(items, announcement, i) {
 | 
				
			||||||
	os.api('i/read-announcement', {
 | 
						items[i] = {
 | 
				
			||||||
		announcementId: announcement.id,
 | 
							...announcement,
 | 
				
			||||||
	}).then(reload);
 | 
							isRead: true,
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
						os.api('i/read-announcement', { announcementId: announcement.id });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const headerActions = $computed(() => []);
 | 
					const headerActions = $computed(() => []);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -96,7 +96,6 @@ provideMetadataReceiver((info) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const announcements = {
 | 
					const announcements = {
 | 
				
			||||||
	endpoint: 'announcements',
 | 
						endpoint: 'announcements',
 | 
				
			||||||
	offsetMode: true,
 | 
					 | 
				
			||||||
	limit: 10,
 | 
						limit: 10,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user