parent
							
								
									04fefb2056
								
							
						
					
					
						commit
						576251200f
					
				|  | @ -1075,9 +1075,6 @@ export interface Locale { | |||
|     "additionalEmojiDictionary": string; | ||||
|     "installed": string; | ||||
|     "branding": string; | ||||
|     "newUserAnnouncementAvailable": string; | ||||
|     "viewAnnouncement": string; | ||||
|     "dialogCloseDuration": string; | ||||
|     "enableServerMachineStats": string; | ||||
|     "enableIdenticonGeneration": string; | ||||
|     "turnOffToImprovePerformance": string; | ||||
|  |  | |||
|  | @ -1072,9 +1072,6 @@ goToMisskey: "Misskeyへ" | |||
| additionalEmojiDictionary: "絵文字の追加辞書" | ||||
| installed: "インストール済み" | ||||
| branding: "ブランディング" | ||||
| newUserAnnouncementAvailable: "新着のあなた宛てのお知らせがあります" | ||||
| viewAnnouncement: "お知らせを見る" | ||||
| dialogCloseDuration: "ダイアログを閉じるまでの待機時間" | ||||
| enableServerMachineStats: "サーバーのマシン情報を公開する" | ||||
| enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" | ||||
| turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。" | ||||
|  |  | |||
|  | @ -1,20 +0,0 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| export class Userannouncement1688647797135 { | ||||
|     name = 'Userannouncement1688647797135' | ||||
| 
 | ||||
|     async up(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "announcement" ADD COLUMN "userId" character varying(32)`); | ||||
|         await queryRunner.query(`ALTER TABLE "announcement" ADD COLUMN "closeDuration" integer NOT NULL DEFAULT 0`); | ||||
|         await queryRunner.query(`CREATE INDEX "IDX_fd25dfe3da37df1715f11ba6ec" ON "announcement" ("userId") `); | ||||
|     } | ||||
| 
 | ||||
|     async down(queryRunner) { | ||||
|         await queryRunner.query(`DROP INDEX "public"."IDX_fd25dfe3da37df1715f11ba6ec"`); | ||||
|         await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "userId"`); | ||||
|         await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "closeDuration"`); | ||||
|     } | ||||
| } | ||||
|  | @ -4,7 +4,7 @@ | |||
|  */ | ||||
| 
 | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { In, IsNull, Not } from 'typeorm'; | ||||
| import { In, Not } from 'typeorm'; | ||||
| import * as Redis from 'ioredis'; | ||||
| import _Ajv from 'ajv'; | ||||
| import { ModuleRef } from '@nestjs/core'; | ||||
|  | @ -250,11 +250,9 @@ export class UserEntityService implements OnModuleInit { | |||
| 			userId: userId, | ||||
| 		}); | ||||
| 
 | ||||
| 		const id = reads.length > 0 ? Not(In(reads.map(read => read.announcementId))) : undefined; | ||||
| 		const count = await this.announcementsRepository.countBy([ | ||||
| 			{ id, userId: IsNull() }, | ||||
| 			{ id, userId: userId }, | ||||
| 		]); | ||||
| 		const count = await this.announcementsRepository.countBy(reads.length > 0 ? { | ||||
| 			id: Not(In(reads.map(read => read.announcementId))), | ||||
| 		} : {}); | ||||
| 
 | ||||
| 		return count > 0; | ||||
| 	} | ||||
|  |  | |||
|  | @ -38,19 +38,6 @@ export class Announcement { | |||
| 	}) | ||||
| 	public imageUrl: string | null; | ||||
| 
 | ||||
| 	@Index() | ||||
| 	@Column('varchar', { | ||||
| 		...id(), | ||||
| 		nullable: true, | ||||
| 	}) | ||||
| 	public userId: string | null; | ||||
| 
 | ||||
| 	@Column('integer', { | ||||
| 		nullable: false, | ||||
| 		default: 0, | ||||
| 	}) | ||||
| 	public closeDuration: number; | ||||
| 
 | ||||
| 	constructor(data: Partial<Announcement>) { | ||||
| 		if (data == null) return; | ||||
| 
 | ||||
|  |  | |||
|  | @ -47,14 +47,6 @@ export const meta = { | |||
| 				type: 'string', | ||||
| 				optional: false, nullable: true, | ||||
| 			}, | ||||
| 			userId: { | ||||
| 				type: 'string', | ||||
| 				optional: false, nullable: true, | ||||
| 			}, | ||||
| 			closeDuration: { | ||||
| 				type: 'number', | ||||
| 				optional: false, nullable: false, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
|  | @ -65,8 +57,6 @@ export const paramDef = { | |||
| 		title: { type: 'string', minLength: 1 }, | ||||
| 		text: { type: 'string', minLength: 1 }, | ||||
| 		imageUrl: { type: 'string', nullable: true, minLength: 1 }, | ||||
| 		userId: { type: 'string', nullable: true, format: 'misskey:id' }, | ||||
| 		closeDuration: { type: 'number', nullable: false }, | ||||
| 	}, | ||||
| 	required: ['title', 'text', 'imageUrl'], | ||||
| } as const; | ||||
|  | @ -88,8 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | |||
| 				title: ps.title, | ||||
| 				text: ps.text, | ||||
| 				imageUrl: ps.imageUrl, | ||||
| 				userId: ps.userId ?? null, | ||||
| 				closeDuration: ps.closeDuration, | ||||
| 			}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0])); | ||||
| 
 | ||||
| 			return Object.assign({}, announcement, { createdAt: announcement.createdAt.toISOString(), updatedAt: null }); | ||||
|  |  | |||
|  | @ -4,12 +4,10 @@ | |||
|  */ | ||||
| 
 | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { In } from 'typeorm'; | ||||
| import type { AnnouncementsRepository, AnnouncementReadsRepository, UsersRepository } from '@/models/index.js'; | ||||
| import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/index.js'; | ||||
| import type { Announcement } from '@/models/entities/Announcement.js'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { QueryService } from '@/core/QueryService.js'; | ||||
| import { UserEntityService } from '@/core/entities/UserEntityService.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| 
 | ||||
| export const meta = { | ||||
|  | @ -53,23 +51,10 @@ export const meta = { | |||
| 					type: 'string', | ||||
| 					optional: false, nullable: true, | ||||
| 				}, | ||||
| 				userId: { | ||||
| 					type: 'string', | ||||
| 					optional: false, nullable: true, | ||||
| 				}, | ||||
| 				user: { | ||||
| 					type: 'object', | ||||
| 					optional: true, nullable: false, | ||||
| 					ref: 'UserLite', | ||||
| 				}, | ||||
| 				reads: { | ||||
| 					type: 'number', | ||||
| 					optional: false, nullable: false, | ||||
| 				}, | ||||
| 				closeDuration: { | ||||
| 					type: 'number', | ||||
| 					optional: false, nullable: false, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
|  | @ -81,7 +66,6 @@ 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' }, | ||||
| 	}, | ||||
| 	required: [], | ||||
| } as const; | ||||
|  | @ -96,21 +80,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | |||
| 		@Inject(DI.announcementReadsRepository) | ||||
| 		private announcementReadsRepository: AnnouncementReadsRepository, | ||||
| 
 | ||||
| 		@Inject(DI.usersRepository) | ||||
| 		private usersRepository: UsersRepository, | ||||
| 
 | ||||
| 		private queryService: QueryService, | ||||
| 		private userEntityService: UserEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const builder = this.announcementsRepository.createQueryBuilder('announcement'); | ||||
| 			if (ps.userId) { | ||||
| 				builder.where('"userId" = :userId', { userId: ps.userId }); | ||||
| 			} else { | ||||
| 				builder.where('"userId" IS NULL'); | ||||
| 			} | ||||
| 
 | ||||
| 			const query = this.queryService.makePaginationQuery(builder, ps.sinceId, ps.untilId); | ||||
| 			const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); | ||||
| 
 | ||||
| 			const announcements = await query.limit(ps.limit).getMany(); | ||||
| 
 | ||||
|  | @ -122,13 +95,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | |||
| 				})); | ||||
| 			} | ||||
| 
 | ||||
| 			const users = await this.usersRepository.findBy({ | ||||
| 				id: In(announcements.map(a => a.userId).filter(id => id != null)), | ||||
| 			}); | ||||
| 			const packedUsers = await this.userEntityService.packMany(users, me, { | ||||
| 				detail: false, | ||||
| 			}); | ||||
| 
 | ||||
| 			return announcements.map(announcement => ({ | ||||
| 				id: announcement.id, | ||||
| 				createdAt: announcement.createdAt.toISOString(), | ||||
|  | @ -136,10 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | |||
| 				title: announcement.title, | ||||
| 				text: announcement.text, | ||||
| 				imageUrl: announcement.imageUrl, | ||||
| 				userId: announcement.userId, | ||||
| 				user: packedUsers.find(user => user.id === announcement.userId), | ||||
| 				reads: reads.get(announcement)!, | ||||
| 				closeDuration: announcement.closeDuration, | ||||
| 			})); | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 
 | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js'; | ||||
| import type { AnnouncementsRepository } from '@/models/index.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { ApiError } from '../../../error.js'; | ||||
| 
 | ||||
|  | @ -31,10 +31,8 @@ export const paramDef = { | |||
| 		title: { type: 'string', minLength: 1 }, | ||||
| 		text: { type: 'string', minLength: 1 }, | ||||
| 		imageUrl: { type: 'string', nullable: true, minLength: 0 }, | ||||
| 		userId: { type: 'string', nullable: true, format: 'misskey:id' }, | ||||
| 		closeDuration: { type: 'number', nullable: false }, | ||||
| 	}, | ||||
| 	required: ['id', 'title', 'text', 'imageUrl', 'closeDuration'], | ||||
| 	required: ['id', 'title', 'text', 'imageUrl'], | ||||
| } as const; | ||||
| 
 | ||||
| // eslint-disable-next-line import/no-default-export
 | ||||
|  | @ -43,27 +41,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | |||
| 	constructor( | ||||
| 		@Inject(DI.announcementsRepository) | ||||
| 		private announcementsRepository: AnnouncementsRepository, | ||||
| 
 | ||||
| 		@Inject(DI.announcementReadsRepository) | ||||
| 		private announcementsReadsRepository: AnnouncementReadsRepository, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const announcement = await this.announcementsRepository.findOneBy({ id: ps.id }); | ||||
| 
 | ||||
| 			if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); | ||||
| 
 | ||||
| 			if (announcement.userId && announcement.userId !== ps.userId) { | ||||
| 				await this.announcementsReadsRepository.delete({ id: announcement.id, userId: announcement.userId }); | ||||
| 			} | ||||
| 
 | ||||
| 			await this.announcementsRepository.update(announcement.id, { | ||||
| 				updatedAt: new Date(), | ||||
| 				title: ps.title, | ||||
| 				text: ps.text, | ||||
| 				/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ | ||||
| 				imageUrl: ps.imageUrl || null, | ||||
| 				userId: ps.userId ?? null, | ||||
| 				closeDuration: ps.closeDuration, | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -53,14 +53,6 @@ export const meta = { | |||
| 					type: 'boolean', | ||||
| 					optional: true, nullable: false, | ||||
| 				}, | ||||
| 				isPrivate: { | ||||
| 					type: 'boolean', | ||||
| 					optional: false, nullable: true, | ||||
| 				}, | ||||
| 				closeDuration: { | ||||
| 					type: 'number', | ||||
| 					optional: false, nullable: false, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
|  | @ -73,7 +65,6 @@ export const paramDef = { | |||
| 		withUnreads: { type: 'boolean', default: false }, | ||||
| 		sinceId: { type: 'string', format: 'misskey:id' }, | ||||
| 		untilId: { type: 'string', format: 'misskey:id' }, | ||||
| 		privateOnly: { type: 'boolean', default: false }, | ||||
| 	}, | ||||
| 	required: [], | ||||
| } as const; | ||||
|  | @ -91,19 +82,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | |||
| 		private queryService: QueryService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const builder = this.announcementsRepository.createQueryBuilder('announcement'); | ||||
| 			if (me) { | ||||
| 				if (ps.privateOnly) { | ||||
| 					builder.where('"userId" = :userId', { userId: me.id }); | ||||
| 				} else { | ||||
| 					builder.where('"userId" IS NULL'); | ||||
| 					builder.orWhere('"userId" = :userId', { userId: me.id }); | ||||
| 				} | ||||
| 			} else { | ||||
| 				builder.where('"userId" IS NULL'); | ||||
| 			} | ||||
| 			const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); | ||||
| 
 | ||||
| 			const query = this.queryService.makePaginationQuery(builder, ps.sinceId, ps.untilId); | ||||
| 			const announcements = await query.limit(ps.limit).getMany(); | ||||
| 
 | ||||
| 			if (me) { | ||||
|  | @ -120,7 +100,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | |||
| 				...a, | ||||
| 				createdAt: a.createdAt.toISOString(), | ||||
| 				updatedAt: a.updatedAt?.toISOString() ?? null, | ||||
| 				isPrivate: !!a.userId, | ||||
| 			})); | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ | |||
|  */ | ||||
| 
 | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { IsNull } from 'typeorm'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { IdService } from '@/core/IdService.js'; | ||||
| import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js'; | ||||
|  | @ -53,18 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | |||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			// Check if announcement exists
 | ||||
| 			const announcementExist = await this.announcementsRepository.exist({ | ||||
| 				where: [ | ||||
| 					{ | ||||
| 						id: ps.announcementId, | ||||
| 						userId: IsNull(), | ||||
| 					}, | ||||
| 					{ | ||||
| 						id: ps.announcementId, | ||||
| 						userId: me.id, | ||||
| 					} | ||||
| 				] | ||||
| 			}); | ||||
| 			const announcementExist = await this.announcementsRepository.exist({ where: { id: ps.announcementId } }); | ||||
| 
 | ||||
| 			if (!announcementExist) { | ||||
| 				throw new ApiError(meta.errors.noSuchAnnouncement); | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import { computed, createApp, watch, markRaw, version as vueVersion, defineAsync | |||
| import { common } from './common'; | ||||
| import { version, ui, lang, updateLocale } from '@/config'; | ||||
| import { i18n, updateI18n } from '@/i18n'; | ||||
| import { confirm, alert, post, popup, toast, api } from '@/os'; | ||||
| import { confirm, alert, post, popup, toast } from '@/os'; | ||||
| import { useStream } from '@/stream'; | ||||
| import * as sound from '@/scripts/sound'; | ||||
| import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; | ||||
|  | @ -251,11 +251,6 @@ export async function mainBoot() { | |||
| 		main.on('myTokenRegenerated', () => { | ||||
| 			signout(); | ||||
| 		}); | ||||
| 
 | ||||
| 		const unreadUserAnnouncementsList = await api('announcements', { privateOnly: true, withUnreads: true }); | ||||
| 		if (unreadUserAnnouncementsList.length > 0) { | ||||
| 			unreadUserAnnouncementsList.forEach((v) => popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementModal.vue')), { title: v.title, text: v.text, closeDuration: v.closeDuration, announcementId: v.id }, {}, 'closed')); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// shortcut
 | ||||
|  |  | |||
|  | @ -1,95 +0,0 @@ | |||
| <!-- | ||||
| SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
| SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
| 
 | ||||
| <template> | ||||
| <MkModal ref="modal" :zPriority="'middle'" @click="closeModal" @closed="$emit('closed')"> | ||||
| 	<div :class="$style.root"> | ||||
| 		<div :class="$style.title"><Mfm :text="props.title"/></div> | ||||
| 		<div :class="$style.text"> | ||||
| 			<Mfm :text="props.text"/> | ||||
| 		</div> | ||||
| 		<MkButton :class="$style.gotIt" primary full :disabled="gotItDisabled" @click="gotIt">{{ i18n.ts.gotIt }}<span v-if="secVisible">({{ sec }})</span></MkButton> | ||||
| 	</div> | ||||
| </MkModal> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { onMounted, ref, shallowRef } from 'vue'; | ||||
| import MkModal from '@/components/MkModal.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { api } from '@/os'; | ||||
| 
 | ||||
| const modal = shallowRef<InstanceType<typeof MkModal>>(); | ||||
| const gotItDisabled = ref(true); | ||||
| const secVisible = ref(true); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	title: string; | ||||
| 	text: string; | ||||
| 	announcementId: string | null; | ||||
| 	closeDuration: number; | ||||
| }>(); | ||||
| 
 | ||||
| const sec = ref(props.closeDuration); | ||||
| 
 | ||||
| async function gotIt() { | ||||
| 	gotItDisabled.value = true; | ||||
| 	if (props.announcementId) { | ||||
| 		await api('i/read-announcement', { announcementId: props.announcementId }); | ||||
| 	} | ||||
| 	modal.value.close(); | ||||
| } | ||||
| 
 | ||||
| function closeModal() { | ||||
| 	if (sec.value === 0) { | ||||
| 		modal.value.close(); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	if (sec.value > 0 ) { | ||||
| 		const waitTimer = setInterval(() => { | ||||
| 			if (sec.value === 0) { | ||||
| 				clearInterval(waitTimer); | ||||
| 				gotItDisabled.value = false; | ||||
| 				secVisible.value = false; | ||||
| 			} else { | ||||
| 				gotItDisabled.value = true; | ||||
| 			} | ||||
| 			sec.value = sec.value - 1; | ||||
| 		}, 1000); | ||||
| 	} else { | ||||
| 		gotItDisabled.value = false; | ||||
| 		secVisible.value = false; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	margin: auto; | ||||
| 	position: relative; | ||||
| 	padding: 32px; | ||||
| 	min-width: 320px; | ||||
| 	max-width: 480px; | ||||
| 	box-sizing: border-box; | ||||
| 	text-align: center; | ||||
| 	background: var(--panel); | ||||
| 	border-radius: var(--radius); | ||||
| } | ||||
| 
 | ||||
| .title { | ||||
| 	font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| .text { | ||||
| 	margin: 1em 0; | ||||
| } | ||||
| 
 | ||||
| .gotIt { | ||||
| 	margin: 8px 0 0 0; | ||||
| } | ||||
| </style> | ||||
|  | @ -8,26 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="900"> | ||||
| 		<div class="_gaps_m"> | ||||
| 			<MkFolder> | ||||
| 				<template #label>{{ i18n.ts.options }}</template> | ||||
| 
 | ||||
| 				<MkFolder> | ||||
| 					<template #label>{{ i18n.ts.specifyUser }}</template> | ||||
| 					<template v-if="user" #suffix>@{{ user.username }}</template> | ||||
| 
 | ||||
| 					<div style="text-align: center;" class="_gaps"> | ||||
| 						<div v-if="user">@{{ user.username }}</div> | ||||
| 						<div> | ||||
| 							<MkButton v-if="user == null" primary rounded inline @click="selectUserFilter">{{ i18n.ts.selectUser }}</MkButton> | ||||
| 							<MkButton v-else danger rounded inline @click="user = null">{{ i18n.ts.remove }}</MkButton> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</MkFolder> | ||||
| 			</MkFolder> | ||||
| 			<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> | ||||
| 					<MkInput v-model="announcement.title"> | ||||
| 						<template #label>{{ i18n.ts.title }}</template> | ||||
| 					</MkInput> | ||||
| 					<MkTextarea v-model="announcement.text"> | ||||
| 						<template #label>{{ i18n.ts.text }}</template> | ||||
|  | @ -35,13 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 					<MkInput v-model="announcement.imageUrl"> | ||||
| 						<template #label>{{ i18n.ts.imageUrl }}</template> | ||||
| 					</MkInput> | ||||
| 					<MkInput v-model="announcement.closeDuration" type="number"> | ||||
| 						<template #label>{{ i18n.ts.dialogCloseDuration }}</template> | ||||
| 						<template #suffix>{{ i18n.ts._time.second }}</template> | ||||
| 					</MkInput> | ||||
| 					<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p> | ||||
| 					<MkUserCardMini v-if="announcement.userId" :user="announcement.user" @click="editUser(announcement)"></MkUserCardMini> | ||||
| 					<MkButton v-else class="button" inline primary @click="editUser(announcement)">{{ i18n.ts.specifyUser }}</MkButton> | ||||
| 					<div class="buttons _buttons"> | ||||
| 						<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> | ||||
| 						<MkButton class="button" inline danger @click="remove(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> | ||||
|  | @ -54,40 +32,17 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { ref, watch } from 'vue'; | ||||
| import { UserLite } from 'misskey-js/built/entities'; | ||||
| import { } from 'vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkInput from '@/components/MkInput.vue'; | ||||
| import MkTextarea from '@/components/MkTextarea.vue'; | ||||
| import MkFolder from '@/components/MkFolder.vue'; | ||||
| import MkUserCardMini from '@/components/MkUserCardMini.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let announcements: any[] = $ref([]); | ||||
| 
 | ||||
| const user = ref<UserLite>(null); | ||||
| const announceTitleEl = $shallowRef<HTMLInputElement | null>(null); | ||||
| 
 | ||||
| function selectUserFilter() { | ||||
| 	os.selectUser().then(_user => { | ||||
| 		user.value = _user; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function editUser(an) { | ||||
| 	os.selectUser().then(_user => { | ||||
| 		an.userId = _user.id; | ||||
| 		an.user = _user; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function insertEmoji(ev: MouseEvent) { | ||||
| 	os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, announceTitleEl); | ||||
| } | ||||
| 
 | ||||
| os.api('admin/announcements/list').then(announcementResponse => { | ||||
| 	announcements = announcementResponse; | ||||
| }); | ||||
|  | @ -98,9 +53,6 @@ function add() { | |||
| 		title: '', | ||||
| 		text: '', | ||||
| 		imageUrl: null, | ||||
| 		userId: null, | ||||
| 		user: null, | ||||
| 		closeDuration: 10, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | @ -145,13 +97,11 @@ function save(announcement) { | |||
| } | ||||
| 
 | ||||
| function refresh() { | ||||
| 	os.api('admin/announcements/list', { userId: user.value?.id }).then(announcementResponse => { | ||||
| 	os.api('admin/announcements/list').then(announcementResponse => { | ||||
| 		announcements = announcementResponse; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| watch(user, refresh); | ||||
| 
 | ||||
| refresh(); | ||||
| 
 | ||||
| const headerActions = $computed(() => [{ | ||||
|  |  | |||
|  | @ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :contentMax="800"> | ||||
| 		<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> | ||||
| 			<section v-for="(announcement, i) in items" :key="announcement.id" class="announcement _panel"> | ||||
| 				<div class="header"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> | ||||
| 				<div class="content"> | ||||
| 					<Mfm :text="announcement.text"/> | ||||
| 					<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/> | ||||
|  | @ -58,11 +58,6 @@ definePageMetadata({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .ruryvtyk { | ||||
| 
 | ||||
| 	> .private { | ||||
| 		border-left: 4px solid olivedrab; | ||||
| 	} | ||||
| 
 | ||||
| 	> .announcement { | ||||
| 		padding: 16px; | ||||
| 
 | ||||
|  | @ -84,16 +79,4 @@ definePageMetadata({ | |||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @keyframes fade { | ||||
| 	0% { | ||||
| 		opacity: 0; | ||||
| 	} | ||||
| 	50% { | ||||
| 		opacity: 1; | ||||
| 	} | ||||
| 	100% { | ||||
| 		opacity: 0; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -31,8 +31,6 @@ type Announcement = { | |||
|     title: string; | ||||
|     imageUrl: string | null; | ||||
|     isRead?: boolean; | ||||
|     isPrivate: boolean; | ||||
|     closeDuration: number; | ||||
| }; | ||||
| 
 | ||||
| // @public (undocumented) | ||||
|  | @ -570,7 +568,6 @@ export type Endpoints = { | |||
|             withUnreads?: boolean; | ||||
|             sinceId?: Announcement['id']; | ||||
|             untilId?: Announcement['id']; | ||||
|             privateOnly?: boolean; | ||||
|         }; | ||||
|         res: Announcement[]; | ||||
|     }; | ||||
|  |  | |||
|  | @ -77,7 +77,7 @@ export type Endpoints = { | |||
| 	'admin/relays/remove': { req: TODO; res: TODO; }; | ||||
| 
 | ||||
| 	// announcements
 | ||||
| 	'announcements': { req: { limit?: number; withUnreads?: boolean; sinceId?: Announcement['id']; untilId?: Announcement['id']; privateOnly?: boolean; }; res: Announcement[]; }; | ||||
| 	'announcements': { req: { limit?: number; withUnreads?: boolean; sinceId?: Announcement['id']; untilId?: Announcement['id']; }; res: Announcement[]; }; | ||||
| 
 | ||||
| 	// antennas
 | ||||
| 	'antennas/create': { req: TODO; res: Antenna; }; | ||||
|  |  | |||
|  | @ -419,8 +419,6 @@ export type Announcement = { | |||
| 	title: string; | ||||
| 	imageUrl: string | null; | ||||
| 	isRead?: boolean; | ||||
| 	isPrivate: boolean; | ||||
| 	closeDuration: number; | ||||
| }; | ||||
| 
 | ||||
| export type Antenna = { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue