@@ -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}\"?"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -254,7 +254,6 @@ export interface Locale {
 | 
			
		||||
    "lookup": string;
 | 
			
		||||
    "announcements": string;
 | 
			
		||||
    "imageUrl": string;
 | 
			
		||||
    "displayOrder": string;
 | 
			
		||||
    "remove": string;
 | 
			
		||||
    "removed": string;
 | 
			
		||||
    "removeAreYouSure": string;
 | 
			
		||||
 
 | 
			
		||||
@@ -251,7 +251,6 @@ noSuchUser: "ユーザーが見つかりません"
 | 
			
		||||
lookup: "照会"
 | 
			
		||||
announcements: "お知らせ"
 | 
			
		||||
imageUrl: "画像URL"
 | 
			
		||||
displayOrder: "表示順"
 | 
			
		||||
remove: "削除"
 | 
			
		||||
removed: "削除しました"
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
	// UIに表示する際の並び順用(大きいほど先頭)
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('integer', {
 | 
			
		||||
		default: 0,
 | 
			
		||||
	})
 | 
			
		||||
	public displayOrder: number;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		...id(),
 | 
			
		||||
 
 | 
			
		||||
@@ -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<typeof meta, typeof paramDef> {
 | 
			
		||||
				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]));
 | 
			
		||||
 
 | 
			
		||||
@@ -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<typeof meta, typeof paramDef> {
 | 
			
		||||
		@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<Announcement, number>();
 | 
			
		||||
 | 
			
		||||
@@ -144,7 +136,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
				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)!,
 | 
			
		||||
 
 | 
			
		||||
@@ -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<typeof meta, typeof paramDef> {
 | 
			
		||||
				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,
 | 
			
		||||
			});
 | 
			
		||||
 
 | 
			
		||||
@@ -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<typeof meta, typeof paramDef> {
 | 
			
		||||
	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<Announcement & { isRead: boolean }>();
 | 
			
		||||
			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,
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
					</div>
 | 
			
		||||
				</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;">
 | 
			
		||||
					<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>
 | 
			
		||||
@@ -35,9 +35,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
					<MkInput v-model="announcement.imageUrl">
 | 
			
		||||
						<template #label>{{ i18n.ts.imageUrl }}</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
					<MkInput v-model="announcement.displayOrder" type="number">
 | 
			
		||||
						<template #label>{{ i18n.ts.displayOrder }}</template>
 | 
			
		||||
					</MkInput>
 | 
			
		||||
					<MkInput v-model="announcement.closeDuration" type="number">
 | 
			
		||||
						<template #label>{{ i18n.ts.dialogCloseDuration }}</template>
 | 
			
		||||
						<template #suffix>{{ i18n.ts._time.second }}</template>
 | 
			
		||||
@@ -51,7 +48,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</section>
 | 
			
		||||
			<MkButton v-if="hasMore" :class="$style.more" :disabled="!hasMore" primary rounded @click="fetch()">{{ i18n.ts.loadMore }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkStickyContainer>
 | 
			
		||||
@@ -70,31 +66,33 @@ import * as os from '@/os';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
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([]);
 | 
			
		||||
 | 
			
		||||
function insertEmoji(ev: MouseEvent): void {
 | 
			
		||||
	os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, announceTitleEl);
 | 
			
		||||
}
 | 
			
		||||
const user = ref<UserLite>(null);
 | 
			
		||||
const announceTitleEl = $shallowRef<HTMLInputElement | null>(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',
 | 
			
		||||
});
 | 
			
		||||
</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>
 | 
			
		||||
	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 | 
			
		||||
	<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 }">
 | 
			
		||||
				<div class="header"><span v-if="$i && !announcement.isRead"><span class="ti ti-speakerphone"></span></span><Mfm :text="announcement.title"/></div>
 | 
			
		||||
				<div class="content">
 | 
			
		||||
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
					<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
 | 
			
		||||
				</div>
 | 
			
		||||
				<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>
 | 
			
		||||
			</section>
 | 
			
		||||
		</MkPagination>
 | 
			
		||||
@@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import MkPagination from '@/components/MkPagination.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
@@ -33,15 +34,16 @@ import { $i } from '@/account';
 | 
			
		||||
 | 
			
		||||
const pagination = {
 | 
			
		||||
	endpoint: 'announcements' as const,
 | 
			
		||||
	offsetMode: true,
 | 
			
		||||
	limit: 10,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function read(items, reload, announcement, i) {
 | 
			
		||||
	items[i].isRead = true;
 | 
			
		||||
	os.api('i/read-announcement', {
 | 
			
		||||
		announcementId: announcement.id,
 | 
			
		||||
	}).then(reload);
 | 
			
		||||
// TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい
 | 
			
		||||
function read(items, announcement, i) {
 | 
			
		||||
	items[i] = {
 | 
			
		||||
		...announcement,
 | 
			
		||||
		isRead: true,
 | 
			
		||||
	};
 | 
			
		||||
	os.api('i/read-announcement', { announcementId: announcement.id });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const headerActions = $computed(() => []);
 | 
			
		||||
 
 | 
			
		||||
@@ -96,7 +96,6 @@ provideMetadataReceiver((info) => {
 | 
			
		||||
 | 
			
		||||
const announcements = {
 | 
			
		||||
	endpoint: 'announcements',
 | 
			
		||||
	offsetMode: true,
 | 
			
		||||
	limit: 10,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user