Feat: 絵文字申請中のやつのテーブルを分けた
Signed-off-by: mattyatea <mattyacocacora0@gmail.com>
This commit is contained in:
		
							parent
							
								
									97590f2567
								
							
						
					
					
						commit
						fe938bf8e6
					
				|  | @ -261,6 +261,7 @@ export interface Locale { | |||
|     "imageUrl": string; | ||||
|     "remove": string; | ||||
|     "removed": string; | ||||
|     "undraftAreYouSure": string; | ||||
|     "removeAreYouSure": string; | ||||
|     "deleteAreYouSure": string; | ||||
|     "resetAreYouSure": string; | ||||
|  |  | |||
|  | @ -1,11 +0,0 @@ | |||
| export class AddEmojiDraftFlag1684236161625 { | ||||
|     name = 'AddEmojiDraftFlag1684236161625' | ||||
| 
 | ||||
|     async up(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "emoji" ADD "draft" boolean NOT NULL DEFAULT false`); | ||||
|     } | ||||
| 
 | ||||
|     async down(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "draft"`); | ||||
|     } | ||||
| } | ||||
|  | @ -86,6 +86,7 @@ import { ClipEntityService } from './entities/ClipEntityService.js'; | |||
| import { DriveFileEntityService } from './entities/DriveFileEntityService.js'; | ||||
| import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js'; | ||||
| import { EmojiEntityService } from './entities/EmojiEntityService.js'; | ||||
| import { EmojiDraftsEntityService } from './entities/EmojiDraftsEntityService.js'; | ||||
| import { FollowingEntityService } from './entities/FollowingEntityService.js'; | ||||
| import { FollowRequestEntityService } from './entities/FollowRequestEntityService.js'; | ||||
| import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js'; | ||||
|  | @ -217,6 +218,7 @@ const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting | |||
| const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService }; | ||||
| const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService }; | ||||
| const $EmojiEntityService: Provider = { provide: 'EmojiEntityService', useExisting: EmojiEntityService }; | ||||
| const $EmojiDraftsEntityService: Provider = { provide: 'EmojiDraftsEntityService', useExisting: EmojiDraftsEntityService }; | ||||
| const $FollowingEntityService: Provider = { provide: 'FollowingEntityService', useExisting: FollowingEntityService }; | ||||
| const $FollowRequestEntityService: Provider = { provide: 'FollowRequestEntityService', useExisting: FollowRequestEntityService }; | ||||
| const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService', useExisting: GalleryLikeEntityService }; | ||||
|  | @ -348,6 +350,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting | |||
| 		DriveFileEntityService, | ||||
| 		DriveFolderEntityService, | ||||
| 		EmojiEntityService, | ||||
| 		EmojiDraftsEntityService, | ||||
| 		FollowingEntityService, | ||||
| 		FollowRequestEntityService, | ||||
| 		GalleryLikeEntityService, | ||||
|  | @ -474,6 +477,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting | |||
| 		$DriveFileEntityService, | ||||
| 		$DriveFolderEntityService, | ||||
| 		$EmojiEntityService, | ||||
| 		$EmojiDraftsEntityService, | ||||
| 		$FollowingEntityService, | ||||
| 		$FollowRequestEntityService, | ||||
| 		$GalleryLikeEntityService, | ||||
|  | @ -600,6 +604,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting | |||
| 		DriveFileEntityService, | ||||
| 		DriveFolderEntityService, | ||||
| 		EmojiEntityService, | ||||
| 		EmojiDraftsEntityService, | ||||
| 		FollowingEntityService, | ||||
| 		FollowRequestEntityService, | ||||
| 		GalleryLikeEntityService, | ||||
|  | @ -725,6 +730,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting | |||
| 		$DriveFileEntityService, | ||||
| 		$DriveFolderEntityService, | ||||
| 		$EmojiEntityService, | ||||
| 		$EmojiDraftsEntityService, | ||||
| 		$FollowingEntityService, | ||||
| 		$FollowRequestEntityService, | ||||
| 		$GalleryLikeEntityService, | ||||
|  |  | |||
|  | @ -12,12 +12,13 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; | |||
| import { GlobalEventService } from '@/core/GlobalEventService.js'; | ||||
| import type { MiDriveFile } from '@/models/DriveFile.js'; | ||||
| import type { MiEmoji } from '@/models/Emoji.js'; | ||||
| import type { EmojisRepository, MiRole, MiUser, DriveFilesRepository } from '@/models/_.js'; | ||||
| import type { EmojisRepository, EmojiDraftsRepository, MiRole, MiUser } from '@/models/_.js'; | ||||
| import { bindThis } from '@/decorators.js'; | ||||
| import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; | ||||
| import { UtilityService } from '@/core/UtilityService.js'; | ||||
| import type { Serialized } from '@/types.js'; | ||||
| import { ModerationLogService } from '@/core/ModerationLogService.js'; | ||||
| import { MiEmojiDraft } from '@/models/EmojiDraft.js'; | ||||
| 
 | ||||
| const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; | ||||
| 
 | ||||
|  | @ -33,8 +34,8 @@ export class CustomEmojiService implements OnApplicationShutdown { | |||
| 		@Inject(DI.emojisRepository) | ||||
| 		private emojisRepository: EmojisRepository, | ||||
| 
 | ||||
| 		@Inject(DI.driveFilesRepository) | ||||
| 		private driveFilesRepository: DriveFilesRepository, | ||||
| 		@Inject(DI.emojiDraftsRepository) | ||||
| 		private emojiDraftsRepository: EmojiDraftsRepository, | ||||
| 
 | ||||
| 		private utilityService: UtilityService, | ||||
| 		private idService: IdService, | ||||
|  | @ -58,6 +59,40 @@ export class CustomEmojiService implements OnApplicationShutdown { | |||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async draft(data: { | ||||
| 		driveFile: MiDriveFile; | ||||
| 		name: string; | ||||
| 		category: string | null; | ||||
| 		aliases: string[]; | ||||
| 		license: string | null; | ||||
| 		isSensitive: boolean; | ||||
| 		localOnly: boolean; | ||||
| 	}, me?: MiUser): Promise<MiEmojiDraft> { | ||||
| 		const emoji = await this.emojiDraftsRepository.insert({ | ||||
| 			id: this.idService.gen(), | ||||
| 			updatedAt: new Date(), | ||||
| 			name: data.name, | ||||
| 			category: data.category, | ||||
| 			aliases: data.aliases, | ||||
| 			originalUrl: data.driveFile.url, | ||||
| 			publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url, | ||||
| 			type: data.driveFile.webpublicType ?? data.driveFile.type, | ||||
| 			license: data.license, | ||||
| 			isSensitive: data.isSensitive, | ||||
| 			localOnly: data.localOnly, | ||||
| 			fileId: data.driveFile.id, | ||||
| 		}).then(x => this.emojiDraftsRepository.findOneByOrFail(x.identifiers[0])); | ||||
| 
 | ||||
| 		if (me) { | ||||
| 			this.moderationLogService.log(me, 'addCustomEmoji', { | ||||
| 				emojiId: emoji.id, | ||||
| 				emoji: emoji, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		return emoji; | ||||
| 	} | ||||
| 	@bindThis | ||||
| 	public async add(data: { | ||||
| 		driveFile: MiDriveFile; | ||||
|  | @ -68,7 +103,6 @@ export class CustomEmojiService implements OnApplicationShutdown { | |||
| 		license: string | null; | ||||
| 		isSensitive: boolean; | ||||
| 		localOnly: boolean; | ||||
|         draft: boolean; | ||||
| 		roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; | ||||
| 	}, moderator?: MiUser): Promise<MiEmoji> { | ||||
| 		const emoji = await this.emojisRepository.insert({ | ||||
|  | @ -85,7 +119,6 @@ export class CustomEmojiService implements OnApplicationShutdown { | |||
| 			isSensitive: data.isSensitive, | ||||
| 			localOnly: data.localOnly, | ||||
| 			roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, | ||||
| 			draft: data.draft, | ||||
| 		}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); | ||||
| 
 | ||||
| 		if (data.host == null) { | ||||
|  | @ -113,44 +146,27 @@ export class CustomEmojiService implements OnApplicationShutdown { | |||
| 		category?: string | null; | ||||
| 		aliases?: string[]; | ||||
| 		license?: string | null; | ||||
|         fileId?: string | null; | ||||
| 		isSensitive?: boolean; | ||||
| 		localOnly?: boolean; | ||||
|         draft: boolean; | ||||
| 		roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; | ||||
| 	}, moderator?: MiUser): Promise<void> { | ||||
| 		const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); | ||||
| 		const driveFile = data.fileId !== null ? await this.driveFilesRepository.findOneBy({ id: data.fileId }) : null; | ||||
| 		const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); | ||||
| 		if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists'); | ||||
| 
 | ||||
| 		if (driveFile !== null) { | ||||
| 			await this.emojisRepository.update(emoji.id, { | ||||
| 				updatedAt: new Date(), | ||||
| 				name: data.name, | ||||
| 				category: data.category, | ||||
| 				aliases: data.aliases, | ||||
| 				license: data.license, | ||||
| 				isSensitive: data.isSensitive, | ||||
| 				localOnly: data.localOnly, | ||||
| 				originalUrl: data.driveFile != null ? data.driveFile.url : undefined, | ||||
| 				publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, | ||||
| 				type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, | ||||
| 				draft: data.draft, | ||||
| 			}); | ||||
| 		} else { | ||||
| 			await this.emojisRepository.update(emoji.id, { | ||||
| 				updatedAt: new Date(), | ||||
| 				name: data.name, | ||||
| 				category: data.category, | ||||
| 				aliases: data.aliases, | ||||
| 				license: data.license, | ||||
| 				isSensitive: data.isSensitive, | ||||
| 				localOnly: data.localOnly, | ||||
| 				draft: data.draft, | ||||
| 				roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, | ||||
| 			}); | ||||
| 		} | ||||
| 		await this.emojisRepository.update(emoji.id, { | ||||
| 			updatedAt: new Date(), | ||||
| 			name: data.name, | ||||
| 			category: data.category, | ||||
| 			aliases: data.aliases, | ||||
| 			license: data.license, | ||||
| 			isSensitive: data.isSensitive, | ||||
| 			localOnly: data.localOnly, | ||||
| 			originalUrl: data.driveFile != null ? data.driveFile.url : undefined, | ||||
| 			publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, | ||||
| 			type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, | ||||
| 			roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, | ||||
| 		}); | ||||
| 
 | ||||
| 		this.localEmojisCache.refresh(); | ||||
| 
 | ||||
|  | @ -179,7 +195,35 @@ export class CustomEmojiService implements OnApplicationShutdown { | |||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 	@bindThis | ||||
| 	public async draftUpdate(id: MiEmoji['id'], data: { | ||||
| 		driveFile?: MiDriveFile; | ||||
| 		name?: string; | ||||
| 		category?: string | null; | ||||
| 		aliases?: string[]; | ||||
| 		license?: string | null; | ||||
| 		isSensitive?: boolean; | ||||
| 		localOnly?: boolean; | ||||
| 	}, moderator?: MiUser): Promise<void> { | ||||
| 		const emoji = await this.emojiDraftsRepository.findOneByOrFail({ id: id }); | ||||
| 		const sameNameEmoji = await this.emojiDraftsRepository.findOneBy({ name: data.name }); | ||||
| 		if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists'); | ||||
| 
 | ||||
| 		await this.emojiDraftsRepository.update(emoji.id, { | ||||
| 			updatedAt: new Date(), | ||||
| 			name: data.name, | ||||
| 			category: data.category, | ||||
| 			aliases: data.aliases, | ||||
| 			license: data.license, | ||||
| 			isSensitive: data.isSensitive, | ||||
| 			localOnly: data.localOnly, | ||||
| 			originalUrl: data.driveFile != null ? data.driveFile.url : undefined, | ||||
| 			publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, | ||||
| 			type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, | ||||
| 		}); | ||||
| 
 | ||||
| 		this.localEmojisCache.refresh(); | ||||
| 	} | ||||
| 	@bindThis | ||||
| 	public async addAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) { | ||||
| 		const emojis = await this.emojisRepository.findBy({ | ||||
|  | @ -287,7 +331,12 @@ export class CustomEmojiService implements OnApplicationShutdown { | |||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 	@bindThis | ||||
| 	public async draftDelete(id: MiEmojiDraft['id']) { | ||||
| 		const emoji = await this.emojiDraftsRepository.findOneByOrFail({ id: id }); | ||||
| 
 | ||||
| 		await this.emojiDraftsRepository.delete(emoji.id); | ||||
| 	} | ||||
| 	@bindThis | ||||
| 	public async deleteBulk(ids: MiEmoji['id'][], moderator?: MiUser) { | ||||
| 		const emojis = await this.emojisRepository.findBy({ | ||||
|  | @ -408,12 +457,20 @@ export class CustomEmojiService implements OnApplicationShutdown { | |||
| 	public checkDuplicate(name: string): Promise<boolean> { | ||||
| 		return this.emojisRepository.exist({ where: { name, host: IsNull() } }); | ||||
| 	} | ||||
| 	@bindThis | ||||
| 	public checkDraftDuplicate(name: string): Promise<boolean> { | ||||
| 		return this.emojiDraftsRepository.exist({ where: { name } }); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public getEmojiById(id: string): Promise<MiEmoji | null> { | ||||
| 		return this.emojisRepository.findOneBy({ id }); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public getEmojiDraftById(id: string): Promise<MiEmojiDraft | null> { | ||||
| 		return this.emojiDraftsRepository.findOneBy({ id }); | ||||
| 	} | ||||
| 	@bindThis | ||||
| 	public dispose(): void { | ||||
| 		this.cache.dispose(); | ||||
|  |  | |||
|  | @ -133,7 +133,13 @@ export class DriveFileEntityService { | |||
| 		} | ||||
| 		return url; | ||||
| 	} | ||||
| 	@bindThis | ||||
| 	public async getFromUrl(url: string): Promise<MiDriveFile | null> { | ||||
| 		const file = await this.driveFilesRepository.findOneBy({ url: url }); | ||||
| 		if (file === null ) return null; | ||||
| 
 | ||||
| 		return file; | ||||
| 	} | ||||
| 	@bindThis | ||||
| 	public async calcDriveUsageOf(user: MiUser['id'] | { id: MiUser['id'] }): Promise<number> { | ||||
| 		const id = typeof user === 'object' ? user.id : user; | ||||
|  |  | |||
|  | @ -0,0 +1,70 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import type { EmojiDraftsRepository } from '@/models/_.js'; | ||||
| import type { Packed } from '@/misc/json-schema.js'; | ||||
| import { bindThis } from '@/decorators.js'; | ||||
| import { MiEmojiDraft } from '@/models/EmojiDraft.js'; | ||||
| 
 | ||||
| @Injectable() | ||||
| export class EmojiDraftsEntityService { | ||||
| 	constructor( | ||||
| 		@Inject(DI.emojiDraftsRepository) | ||||
| 		private emojiDraftsRepository: EmojiDraftsRepository, | ||||
| 	) { | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async packSimple( | ||||
| 		src: MiEmojiDraft['id'] | MiEmojiDraft, | ||||
| 	): Promise<Packed<'EmojiDraftSimple'>> { | ||||
| 		const emoji = typeof src === 'object' ? src : await this.emojiDraftsRepository.findOneByOrFail({ id: src }); | ||||
| 
 | ||||
| 		return { | ||||
| 			aliases: emoji.aliases, | ||||
| 			name: emoji.name, | ||||
| 			category: emoji.category, | ||||
| 			// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
 | ||||
| 			url: emoji.publicUrl, | ||||
| 			isSensitive: emoji.isSensitive ? true : undefined, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public packSimpleMany( | ||||
| 		emojis: any[], | ||||
| 	) { | ||||
| 		return Promise.all(emojis.map(x => this.packSimple(x))); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async packDetailed( | ||||
| 		src: MiEmojiDraft['id'] | MiEmojiDraft, | ||||
| 	): Promise<Packed<'EmojiDraftDetailed'>> { | ||||
| 		const emoji = typeof src === 'object' ? src : await this.emojiDraftsRepository.findOneByOrFail({ id: src }); | ||||
| 
 | ||||
| 		return { | ||||
| 			id: emoji.id, | ||||
| 			aliases: emoji.aliases, | ||||
| 			name: emoji.name, | ||||
| 			category: emoji.category, | ||||
| 			url: emoji.publicUrl, | ||||
| 			license: emoji.license, | ||||
| 			isSensitive: emoji.isSensitive, | ||||
| 			localOnly: emoji.localOnly, | ||||
| 			fileId: emoji.fileId, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public packDetailedMany( | ||||
| 		emojis: any[], | ||||
| 	) { | ||||
| 		return Promise.all(emojis.map(x => this.packDetailed(x))); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -33,7 +33,6 @@ export class EmojiEntityService { | |||
| 			url: emoji.publicUrl || emoji.originalUrl, | ||||
| 			isSensitive: emoji.isSensitive ? true : undefined, | ||||
| 			roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, | ||||
| 			draft: emoji.draft, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
|  | @ -62,7 +61,6 @@ export class EmojiEntityService { | |||
| 			isSensitive: emoji.isSensitive, | ||||
| 			localOnly: emoji.localOnly, | ||||
| 			roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, | ||||
| 			draft: emoji.draft, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -39,6 +39,7 @@ export const DI = { | |||
| 	followRequestsRepository: Symbol('followRequestsRepository'), | ||||
| 	instancesRepository: Symbol('instancesRepository'), | ||||
| 	emojisRepository: Symbol('emojisRepository'), | ||||
| 	emojiDraftsRepository: Symbol('emojiDraftsRepository'), | ||||
| 	driveFilesRepository: Symbol('driveFilesRepository'), | ||||
| 	driveFoldersRepository: Symbol('driveFoldersRepository'), | ||||
| 	metasRepository: Symbol('metasRepository'), | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ import { packedClipSchema } from '@/models/json-schema/clip.js'; | |||
| import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js'; | ||||
| import { packedQueueCountSchema } from '@/models/json-schema/queue.js'; | ||||
| import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; | ||||
| import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js'; | ||||
| import { packedEmojiDetailedSchema, packedEmojiDraftSimpleSchema, packedEmojiSimpleSchema, packedEmojiDraftDetailedSchema } from '@/models/json-schema/emoji.js'; | ||||
| import { packedFlashSchema } from '@/models/json-schema/flash.js'; | ||||
| import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; | ||||
| 
 | ||||
|  | @ -69,7 +69,9 @@ export const refs = { | |||
| 	FederationInstance: packedFederationInstanceSchema, | ||||
| 	GalleryPost: packedGalleryPostSchema, | ||||
| 	EmojiSimple: packedEmojiSimpleSchema, | ||||
| 	EmojiDraftSimple: packedEmojiDraftSimpleSchema, | ||||
| 	EmojiDetailed: packedEmojiDetailedSchema, | ||||
| 	EmojiDraftDetailed: packedEmojiDraftDetailedSchema, | ||||
| 	Flash: packedFlashSchema, | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -81,10 +81,4 @@ export class MiEmoji { | |||
| 		array: true, length: 128, default: '{}', | ||||
| 	}) | ||||
| 	public roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; | ||||
| 
 | ||||
| 	@Column('boolean', { | ||||
| 		default: false, | ||||
| 		nullable: false, | ||||
| 	}) | ||||
| 	public draft: boolean; | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,72 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; | ||||
| import { id } from './util/id.js'; | ||||
| 
 | ||||
| @Entity('emoji_draft') | ||||
| @Index(['name'], { unique: true }) | ||||
| export class MiEmojiDraft { | ||||
| 	@PrimaryColumn(id()) | ||||
| 	public id: string; | ||||
| 
 | ||||
| 	@Column('timestamp with time zone', { | ||||
| 		nullable: true, | ||||
| 	}) | ||||
| 	public updatedAt: Date | null; | ||||
| 
 | ||||
| 	@Index() | ||||
| 	@Column('varchar', { | ||||
| 		length: 128, | ||||
| 	}) | ||||
| 	public name: string; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 128, nullable: true, | ||||
| 	}) | ||||
| 	public category: string | null; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 512, | ||||
| 	}) | ||||
| 	public originalUrl: string; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 512, | ||||
| 		default: '', | ||||
| 	}) | ||||
| 	public publicUrl: string; | ||||
| 
 | ||||
| 	// publicUrlの方のtypeが入る
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 64, nullable: true, | ||||
| 	}) | ||||
| 	public type: string | null; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		array: true, length: 128, default: '{}', | ||||
| 	}) | ||||
| 	public aliases: string[]; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 1024, nullable: true, | ||||
| 	}) | ||||
| 	public license: string | null; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 1024, nullable: false, | ||||
| 	}) | ||||
| 	public fileId: string; | ||||
| 
 | ||||
| 	@Column('boolean', { | ||||
| 		default: false, | ||||
| 	}) | ||||
| 	public localOnly: boolean; | ||||
| 
 | ||||
| 	@Column('boolean', { | ||||
| 		default: false, | ||||
| 	}) | ||||
| 	public isSensitive: boolean; | ||||
| } | ||||
|  | @ -5,7 +5,7 @@ | |||
| 
 | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js'; | ||||
| import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiEmojiDraft, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js'; | ||||
| import type { DataSource } from 'typeorm'; | ||||
| import type { Provider } from '@nestjs/common'; | ||||
| 
 | ||||
|  | @ -165,6 +165,12 @@ const $emojisRepository: Provider = { | |||
| 	inject: [DI.db], | ||||
| }; | ||||
| 
 | ||||
| const $emojiDraftsRepository: Provider = { | ||||
| 	provide: DI.emojiDraftsRepository, | ||||
| 	useFactory: (db: DataSource) => db.getRepository(MiEmojiDraft), | ||||
| 	inject: [DI.db], | ||||
| }; | ||||
| 
 | ||||
| const $driveFilesRepository: Provider = { | ||||
| 	provide: DI.driveFilesRepository, | ||||
| 	useFactory: (db: DataSource) => db.getRepository(MiDriveFile), | ||||
|  | @ -423,6 +429,7 @@ const $userMemosRepository: Provider = { | |||
| 		$followRequestsRepository, | ||||
| 		$instancesRepository, | ||||
| 		$emojisRepository, | ||||
| 		$emojiDraftsRepository, | ||||
| 		$driveFilesRepository, | ||||
| 		$driveFoldersRepository, | ||||
| 		$metasRepository, | ||||
|  | @ -489,6 +496,7 @@ const $userMemosRepository: Provider = { | |||
| 		$followRequestsRepository, | ||||
| 		$instancesRepository, | ||||
| 		$emojisRepository, | ||||
| 		$emojiDraftsRepository, | ||||
| 		$driveFilesRepository, | ||||
| 		$driveFoldersRepository, | ||||
| 		$metasRepository, | ||||
|  |  | |||
|  | @ -67,6 +67,7 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js'; | |||
| import { MiFlash } from '@/models/Flash.js'; | ||||
| import { MiFlashLike } from '@/models/FlashLike.js'; | ||||
| import { MiUserListFavorite } from '@/models/UserListFavorite.js'; | ||||
| import { MiEmojiDraft } from '@/models/EmojiDraft.js'; | ||||
| import type { Repository } from 'typeorm'; | ||||
| 
 | ||||
| export { | ||||
|  | @ -87,6 +88,7 @@ export { | |||
| 	MiDriveFile, | ||||
| 	MiDriveFolder, | ||||
| 	MiEmoji, | ||||
| 	MiEmojiDraft, | ||||
| 	MiFollowing, | ||||
| 	MiFollowRequest, | ||||
| 	MiGalleryLike, | ||||
|  | @ -153,6 +155,7 @@ export type ClipFavoritesRepository = Repository<MiClipFavorite>; | |||
| export type DriveFilesRepository = Repository<MiDriveFile>; | ||||
| export type DriveFoldersRepository = Repository<MiDriveFolder>; | ||||
| export type EmojisRepository = Repository<MiEmoji>; | ||||
| export type EmojiDraftsRepository = Repository<MiEmojiDraft>; | ||||
| export type FollowingsRepository = Repository<MiFollowing>; | ||||
| export type FollowRequestsRepository = Repository<MiFollowRequest>; | ||||
| export type GalleryLikesRepository = Repository<MiGalleryLike>; | ||||
|  |  | |||
|  | @ -40,10 +40,36 @@ export const packedEmojiSimpleSchema = { | |||
| 				format: 'id', | ||||
| 			}, | ||||
| 		}, | ||||
| 		draft: { | ||||
| 			type: 'boolean', | ||||
| 	}, | ||||
| } as const; | ||||
| export const packedEmojiDraftSimpleSchema = { | ||||
| 	type: 'object', | ||||
| 	properties: { | ||||
| 		aliases: { | ||||
| 			type: 'array', | ||||
| 			optional: false, nullable: false, | ||||
| 			items: { | ||||
| 				type: 'string', | ||||
| 				optional: false, nullable: false, | ||||
| 				format: 'id', | ||||
| 			}, | ||||
| 		}, | ||||
| 		name: { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: false, | ||||
| 		}, | ||||
| 		category: { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: true, | ||||
| 		}, | ||||
| 		url: { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: false, | ||||
| 		}, | ||||
| 		isSensitive: { | ||||
| 			type: 'boolean', | ||||
| 			optional: true, nullable: false, | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
| 
 | ||||
|  | @ -85,10 +111,6 @@ export const packedEmojiDetailedSchema = { | |||
| 			type: 'string', | ||||
| 			optional: false, nullable: true, | ||||
| 		}, | ||||
| 		draft: { | ||||
| 			type: 'boolean', | ||||
| 			optional: false, nullable: true, | ||||
| 		}, | ||||
| 		isSensitive: { | ||||
| 			type: 'boolean', | ||||
| 			optional: false, nullable: false, | ||||
|  | @ -108,3 +130,51 @@ export const packedEmojiDetailedSchema = { | |||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
| 
 | ||||
| export const packedEmojiDraftDetailedSchema = { | ||||
| 	type: 'object', | ||||
| 	properties: { | ||||
| 		id: { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: false, | ||||
| 			format: 'id', | ||||
| 		}, | ||||
| 		aliases: { | ||||
| 			type: 'array', | ||||
| 			optional: false, nullable: false, | ||||
| 			items: { | ||||
| 				type: 'string', | ||||
| 				optional: false, nullable: false, | ||||
| 				format: 'id', | ||||
| 			}, | ||||
| 		}, | ||||
| 		name: { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: false, | ||||
| 		}, | ||||
| 		category: { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: true, | ||||
| 		}, | ||||
| 		url: { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: false, | ||||
| 		}, | ||||
| 		license: { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: true, | ||||
| 		}, | ||||
| 		isSensitive: { | ||||
| 			type: 'boolean', | ||||
| 			optional: false, nullable: false, | ||||
| 		}, | ||||
| 		localOnly: { | ||||
| 			type: 'boolean', | ||||
| 			optional: false, nullable: false, | ||||
| 		}, | ||||
| 		fileId: { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: false, | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ import { MiClipFavorite } from '@/models/ClipFavorite.js'; | |||
| import { MiDriveFile } from '@/models/DriveFile.js'; | ||||
| import { MiDriveFolder } from '@/models/DriveFolder.js'; | ||||
| import { MiEmoji } from '@/models/Emoji.js'; | ||||
| import { MiEmojiDraft } from '@/models/EmojiDraft.js'; | ||||
| import { MiFollowing } from '@/models/Following.js'; | ||||
| import { MiFollowRequest } from '@/models/FollowRequest.js'; | ||||
| import { MiGalleryLike } from '@/models/GalleryLike.js'; | ||||
|  | @ -160,6 +161,7 @@ export const entities = [ | |||
| 	MiPoll, | ||||
| 	MiPollVote, | ||||
| 	MiEmoji, | ||||
| 	MiEmojiDraft, | ||||
| 	MiHashtag, | ||||
| 	MiSwSubscription, | ||||
| 	MiAbuseUserReport, | ||||
|  |  | |||
|  | @ -37,6 +37,7 @@ import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-al | |||
| import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js'; | ||||
| import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js'; | ||||
| import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; | ||||
| import * as ep___admin_emoji_draftUpdate from './endpoints/admin/emoji/draft-update.js'; | ||||
| import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; | ||||
| import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; | ||||
| import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; | ||||
|  | @ -243,6 +244,7 @@ import * as ep___invite_list from './endpoints/invite/list.js'; | |||
| import * as ep___invite_limit from './endpoints/invite/limit.js'; | ||||
| import * as ep___meta from './endpoints/meta.js'; | ||||
| import * as ep___emojis from './endpoints/emojis.js'; | ||||
| import * as ep___emojiDrafts from './endpoints/emoji-drafts.js'; | ||||
| import * as ep___emoji from './endpoints/emoji.js'; | ||||
| import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; | ||||
| import * as ep___mute_create from './endpoints/mute/create.js'; | ||||
|  | @ -388,6 +390,7 @@ const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-ali | |||
| const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default }; | ||||
| const $admin_emoji_setLicenseBulk: Provider = { provide: 'ep:admin/emoji/set-license-bulk', useClass: ep___admin_emoji_setLicenseBulk.default }; | ||||
| const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default }; | ||||
| const $admin_emoji_draftUpdate: Provider = { provide: 'ep:admin/emoji/draft-update', useClass: ep___admin_emoji_draftUpdate.default }; | ||||
| const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default }; | ||||
| const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default }; | ||||
| const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default }; | ||||
|  | @ -594,6 +597,7 @@ const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invit | |||
| const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default }; | ||||
| const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; | ||||
| const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default }; | ||||
| const $emoji_drafts: Provider = { provide: 'ep:emoji-drafts', useClass: ep___emojiDrafts.default }; | ||||
| const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default }; | ||||
| const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default }; | ||||
| const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default }; | ||||
|  | @ -743,6 +747,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention | |||
| 		$admin_emoji_setCategoryBulk, | ||||
| 		$admin_emoji_setLicenseBulk, | ||||
| 		$admin_emoji_update, | ||||
| 		$admin_emoji_draftUpdate, | ||||
| 		$admin_federation_deleteAllFiles, | ||||
| 		$admin_federation_refreshRemoteInstanceMetadata, | ||||
| 		$admin_federation_removeAllFollowing, | ||||
|  | @ -949,6 +954,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention | |||
| 		$invite_limit, | ||||
| 		$meta, | ||||
| 		$emojis, | ||||
| 		$emoji_drafts, | ||||
| 		$emoji, | ||||
| 		$miauth_genToken, | ||||
| 		$mute_create, | ||||
|  | @ -1092,6 +1098,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention | |||
| 		$admin_emoji_setCategoryBulk, | ||||
| 		$admin_emoji_setLicenseBulk, | ||||
| 		$admin_emoji_update, | ||||
| 		$admin_emoji_draftUpdate, | ||||
| 		$admin_federation_deleteAllFiles, | ||||
| 		$admin_federation_refreshRemoteInstanceMetadata, | ||||
| 		$admin_federation_removeAllFollowing, | ||||
|  | @ -1298,6 +1305,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention | |||
| 		$invite_limit, | ||||
| 		$meta, | ||||
| 		$emojis, | ||||
| 		$emoji_drafts, | ||||
| 		$emoji, | ||||
| 		$miauth_genToken, | ||||
| 		$mute_create, | ||||
|  |  | |||
|  | @ -37,6 +37,7 @@ import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-al | |||
| import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js'; | ||||
| import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js'; | ||||
| import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; | ||||
| import * as ep___admin_emoji_draftUpdate from './endpoints/admin/emoji/draft-update.js'; | ||||
| import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; | ||||
| import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; | ||||
| import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; | ||||
|  | @ -243,6 +244,7 @@ import * as ep___invite_list from './endpoints/invite/list.js'; | |||
| import * as ep___invite_limit from './endpoints/invite/limit.js'; | ||||
| import * as ep___meta from './endpoints/meta.js'; | ||||
| import * as ep___emojis from './endpoints/emojis.js'; | ||||
| import * as ep___emojiDrafts from './endpoints/emoji-drafts.js'; | ||||
| import * as ep___emoji from './endpoints/emoji.js'; | ||||
| import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; | ||||
| import * as ep___mute_create from './endpoints/mute/create.js'; | ||||
|  | @ -386,6 +388,7 @@ const eps = [ | |||
| 	['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk], | ||||
| 	['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk], | ||||
| 	['admin/emoji/update', ep___admin_emoji_update], | ||||
| 	['admin/emoji/draft-update', ep___admin_emoji_draftUpdate], | ||||
| 	['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles], | ||||
| 	['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata], | ||||
| 	['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing], | ||||
|  | @ -592,6 +595,7 @@ const eps = [ | |||
| 	['invite/limit', ep___invite_limit], | ||||
| 	['meta', ep___meta], | ||||
| 	['emojis', ep___emojis], | ||||
| 	['emoji-drafts', ep___emojiDrafts], | ||||
| 	['emoji', ep___emoji], | ||||
| 	['miauth/gen-token', ep___miauth_genToken], | ||||
| 	['mute/create', ep___mute_create], | ||||
|  |  | |||
|  | @ -18,6 +18,11 @@ export const meta = { | |||
| 			code: 'NO_SUCH_FILE', | ||||
| 			id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', | ||||
| 		}, | ||||
| 		duplicateName: { | ||||
| 			message: 'Duplicate name.', | ||||
| 			code: 'DUPLICATE_NAME', | ||||
| 			id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
| 
 | ||||
|  | @ -55,11 +60,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | |||
| 		private moderationLogService: ModerationLogService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); | ||||
| 			const isDraftDuplicate = await this.customEmojiService.checkDraftDuplicate(ps.name); | ||||
| 
 | ||||
| 			if (isDuplicate || isDraftDuplicate) throw new ApiError(meta.errors.duplicateName); | ||||
| 			const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); | ||||
| 
 | ||||
| 			if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); | ||||
| 
 | ||||
| 			const emoji = await this.customEmojiService.add({ | ||||
| 			const emoji = await this.customEmojiService.draft({ | ||||
| 				driveFile, | ||||
| 				name: ps.name, | ||||
| 				category: ps.category ?? null, | ||||
|  | @ -67,9 +76,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | |||
| 				license: ps.license ?? null, | ||||
| 				isSensitive: ps.isSensitive ?? false, | ||||
| 				localOnly: ps.localOnly ?? false, | ||||
| 				host: null, | ||||
| 				draft: true, | ||||
| 				roleIdsThatCanBeUsedThisEmojiAsReaction: [], | ||||
| 			}); | ||||
| 
 | ||||
| 			await this.moderationLogService.log(me, 'addCustomEmoji', { | ||||
|  |  | |||
|  | @ -51,7 +51,7 @@ export const paramDef = { | |||
| 			type: 'string', | ||||
| 		} }, | ||||
| 	}, | ||||
| 	required: ['name', 'fileId', 'draft'], | ||||
| 	required: ['name', 'fileId'], | ||||
| } as const; | ||||
| 
 | ||||
| // TODO: ロジックをサービスに切り出す
 | ||||
|  | @ -82,7 +82,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 				license: ps.license ?? null, | ||||
| 				isSensitive: ps.isSensitive ?? false, | ||||
| 				localOnly: ps.localOnly ?? false, | ||||
|                 draft: false, | ||||
| 				roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], | ||||
| 			}, me); | ||||
| 
 | ||||
|  |  | |||
|  | @ -36,7 +36,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private customEmojiService: CustomEmojiService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.customEmojiService.delete(ps.id, me); | ||||
| 			const emoji = await this.customEmojiService.getEmojiById(ps.id); | ||||
| 			const draftEmoji = await this.customEmojiService.getEmojiDraftById(ps.id); | ||||
| 			if (emoji != null) { | ||||
| 				await this.customEmojiService.delete(ps.id, me); | ||||
| 			} | ||||
| 			if (draftEmoji != null) { | ||||
| 				await this.customEmojiService.draftDelete(ps.id); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,120 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; | ||||
| import { CustomEmojiService } from '@/core/CustomEmojiService.js'; | ||||
| import type { DriveFilesRepository, EmojiDraftsRepository } from '@/models/_.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { ApiError } from '../../../error.js'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['admin'], | ||||
| 
 | ||||
| 	requireCredential: true, | ||||
| 	requireRolePolicy: 'canManageCustomEmojis', | ||||
| 
 | ||||
| 	errors: { | ||||
| 		noSuchEmoji: { | ||||
| 			message: 'No such emoji.', | ||||
| 			code: 'NO_SUCH_EMOJI', | ||||
| 			id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8', | ||||
| 		}, | ||||
| 		noSuchFile: { | ||||
| 			message: 'No such file.', | ||||
| 			code: 'NO_SUCH_FILE', | ||||
| 			id: '14fb9fd9-0731-4e2f-aeb9-f09e4740333d', | ||||
| 		}, | ||||
| 		sameNameEmojiExists: { | ||||
| 			message: 'Emoji that have same name already exists.', | ||||
| 			code: 'SAME_NAME_EMOJI_EXISTS', | ||||
| 			id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8', | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
| 
 | ||||
| export const paramDef = { | ||||
| 	type: 'object', | ||||
| 	properties: { | ||||
| 		id: { type: 'string', format: 'misskey:id' }, | ||||
| 		name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, | ||||
| 		fileId: { type: 'string', format: 'misskey:id' }, | ||||
| 		category: { | ||||
| 			type: 'string', | ||||
| 			nullable: true, | ||||
| 			description: 'Use `null` to reset the category.', | ||||
| 		}, | ||||
| 		aliases: { type: 'array', items: { | ||||
| 			type: 'string', | ||||
| 		} }, | ||||
| 		license: { type: 'string', nullable: true }, | ||||
| 		isSensitive: { type: 'boolean' }, | ||||
| 		localOnly: { type: 'boolean' }, | ||||
| 		roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { | ||||
| 			type: 'string', | ||||
| 		} }, | ||||
| 		draft: { type: 'boolean' }, | ||||
| 	}, | ||||
| 	required: ['id', 'name', 'aliases'], | ||||
| } as const; | ||||
| 
 | ||||
| @Injectable() | ||||
| export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | ||||
| 	constructor( | ||||
| 		@Inject(DI.driveFilesRepository) | ||||
| 		private driveFilesRepository: DriveFilesRepository, | ||||
| 
 | ||||
| 		@Inject(DI.emojiDraftsRepository) | ||||
| 		private emojiDraftsRepository: EmojiDraftsRepository, | ||||
| 
 | ||||
| 		private customEmojiService: CustomEmojiService, | ||||
| 		private driveFileEntityService: DriveFileEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			let driveFile; | ||||
| 			const isDraft = !!ps.draft; | ||||
| 			if (ps.fileId) { | ||||
| 				driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); | ||||
| 				if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); | ||||
| 			} | ||||
| 
 | ||||
| 			const emoji = await this.customEmojiService.getEmojiDraftById(ps.id); | ||||
| 			if (emoji != null) { | ||||
| 				if (ps.name !== emoji.name) { | ||||
| 					const isDuplicate = await this.customEmojiService.checkDraftDuplicate(ps.name); | ||||
| 					if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists); | ||||
| 				} | ||||
| 			} else { | ||||
| 				throw new ApiError(meta.errors.noSuchEmoji); | ||||
| 			} | ||||
| 			if (!isDraft) { | ||||
| 				const file = await this.driveFileEntityService.getFromUrl(emoji.originalUrl); | ||||
| 				if (file === null) throw new ApiError(meta.errors.noSuchFile); | ||||
| 				await this.customEmojiService.add({ | ||||
| 					driveFile: file, | ||||
| 					name: ps.name, | ||||
| 					category: ps.category ?? null, | ||||
| 					aliases: ps.aliases ?? [], | ||||
| 					host: null, | ||||
| 					license: ps.license ?? null, | ||||
| 					isSensitive: ps.isSensitive ?? false, | ||||
| 					localOnly: ps.localOnly ?? false, | ||||
| 					roleIdsThatCanBeUsedThisEmojiAsReaction: [], | ||||
| 				}, me); | ||||
| 				await this.customEmojiService.draftDelete(ps.id); | ||||
| 			} else { | ||||
| 				await this.customEmojiService.draftUpdate(ps.id, { | ||||
| 					name: ps.name, | ||||
| 					category: ps.category ?? null, | ||||
| 					aliases: ps.aliases ?? [], | ||||
| 					license: ps.license ?? null, | ||||
| 					isSensitive: ps.isSensitive ?? false, | ||||
| 					localOnly: ps.localOnly ?? false, | ||||
| 				}, me); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | @ -5,8 +5,9 @@ | |||
| 
 | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; | ||||
| import { CustomEmojiService } from '@/core/CustomEmojiService.js'; | ||||
| import type { DriveFilesRepository } from '@/models/_.js'; | ||||
| import type { DriveFilesRepository, EmojiDraftsRepository } from '@/models/_.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { ApiError } from '../../../error.js'; | ||||
| 
 | ||||
|  | @ -57,7 +58,7 @@ export const paramDef = { | |||
| 		} }, | ||||
| 		draft: { type: 'boolean' }, | ||||
| 	}, | ||||
| 	required: ['id', 'name', 'aliases', 'draft'], | ||||
| 	required: ['id', 'name', 'aliases'], | ||||
| } as const; | ||||
| 
 | ||||
| @Injectable() | ||||
|  | @ -67,10 +68,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private driveFilesRepository: DriveFilesRepository, | ||||
| 
 | ||||
| 		private customEmojiService: CustomEmojiService, | ||||
| 		private driveFileEntityService: DriveFileEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			let driveFile; | ||||
| 
 | ||||
| 			const isDraft = !!ps.draft; | ||||
| 			if (ps.fileId) { | ||||
| 				driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); | ||||
| 				if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); | ||||
|  | @ -85,18 +87,31 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 				throw new ApiError(meta.errors.noSuchEmoji); | ||||
| 			} | ||||
| 
 | ||||
| 			await this.customEmojiService.update(ps.id, { | ||||
| 				driveFile, | ||||
| 				name: ps.name, | ||||
| 				category: ps.category ?? null, | ||||
| 				aliases: ps.aliases, | ||||
| 				license: ps.license ?? null, | ||||
| 				isSensitive: ps.isSensitive, | ||||
| 				localOnly: ps.localOnly, | ||||
| 				roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, | ||||
| 				fileId: ps.fileId ?? null, | ||||
| 				draft: ps.draft, | ||||
| 			}, me); | ||||
| 			if (!isDraft) { | ||||
| 				await this.customEmojiService.update(ps.id, { | ||||
| 					driveFile, | ||||
| 					name: ps.name, | ||||
| 					category: ps.category ?? null, | ||||
| 					aliases: ps.aliases, | ||||
| 					license: ps.license ?? null, | ||||
| 					isSensitive: ps.isSensitive, | ||||
| 					localOnly: ps.localOnly, | ||||
| 					roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, | ||||
| 				}, me); | ||||
| 			} else { | ||||
| 				const file = await this.driveFileEntityService.getFromUrl(emoji.originalUrl); | ||||
| 				if (file === null) throw new ApiError(meta.errors.noSuchFile); | ||||
| 				await this.customEmojiService.draft({ | ||||
| 					driveFile: file, | ||||
| 					name: ps.name, | ||||
| 					category: ps.category ?? null, | ||||
| 					aliases: ps.aliases ?? [], | ||||
| 					license: ps.license ?? null, | ||||
| 					isSensitive: ps.isSensitive ?? false, | ||||
| 					localOnly: ps.localOnly ?? false, | ||||
| 				}, me); | ||||
| 				await this.customEmojiService.delete(ps.id); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,65 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| import { IsNull } from 'typeorm'; | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import type { EmojiDraftsRepository } from '@/models/_.js'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { EmojiDraftsEntityService } from '@/core/entities/EmojiDraftsEntityService.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['meta'], | ||||
| 
 | ||||
| 	requireCredential: false, | ||||
| 	allowGet: true, | ||||
| 	cacheSec: 3600, | ||||
| 
 | ||||
| 	res: { | ||||
| 		type: 'object', | ||||
| 		optional: false, nullable: false, | ||||
| 		properties: { | ||||
| 			emojis: { | ||||
| 				type: 'array', | ||||
| 				optional: false, nullable: false, | ||||
| 				items: { | ||||
| 					type: 'object', | ||||
| 					optional: false, nullable: false, | ||||
| 					ref: 'EmojiDraftDetailed', | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
| 
 | ||||
| export const paramDef = { | ||||
| 	type: 'object', | ||||
| 	properties: { | ||||
| 	}, | ||||
| 	required: [], | ||||
| } as const; | ||||
| 
 | ||||
| @Injectable() | ||||
| export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | ||||
| 	constructor( | ||||
| 		@Inject(DI.emojiDraftsRepository) | ||||
| 		private emojiDraftsRepository: EmojiDraftsRepository, | ||||
| 
 | ||||
| 		private emojiDraftsEntityService: EmojiDraftsEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async () => { | ||||
| 			const emojis = await this.emojiDraftsRepository.find({ | ||||
| 				order: { | ||||
| 					category: 'ASC', | ||||
| 					name: 'ASC', | ||||
| 				}, | ||||
| 			}); | ||||
| 
 | ||||
| 			return { | ||||
| 				emojis: await this.emojiDraftsEntityService.packDetailedMany(emojis), | ||||
| 			}; | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | @ -33,6 +33,7 @@ export const moderationLogTypes = [ | |||
| 	'unsuspend', | ||||
| 	'updateUserNote', | ||||
| 	'addCustomEmoji', | ||||
| 	'requestCustomEmoji', | ||||
| 	'updateCustomEmoji', | ||||
| 	'deleteCustomEmoji', | ||||
| 	'assignRole', | ||||
|  | @ -88,6 +89,10 @@ export type ModerationLogPayloads = { | |||
| 		emojiId: string; | ||||
| 		emoji: any; | ||||
| 	}; | ||||
| 	requestCustomEmoji: { | ||||
| 		emojiId: string; | ||||
| 		emoji: any; | ||||
| 	}; | ||||
| 	updateCustomEmoji: { | ||||
| 		emojiId: string; | ||||
| 		before: any; | ||||
|  |  | |||
|  | @ -45,18 +45,19 @@ const emojisDraftPaginationComponent = shallowRef<InstanceType<typeof MkPaginati | |||
| const query = ref(null); | ||||
| 
 | ||||
| const paginationDraft = { | ||||
| 	endpoint: 'admin/emoji/list' as const, | ||||
| 	endpoint: 'emoji-drafts' as const, | ||||
| 	limit: 30, | ||||
| 	params: computed(() => ({ | ||||
| 		query: (query.value && query.value !== '') ? query.value : null, | ||||
| 		draft: true, | ||||
| 	})), | ||||
| }; | ||||
| 
 | ||||
| const editDraft = (emoji) => { | ||||
| 	emoji.isDraft = true; | ||||
| 	os.popup(defineAsyncComponent(() => import('@/components/MkEmojiEditDialog.vue')), { | ||||
| 		emoji: emoji, | ||||
| 		isRequest: false, | ||||
| 		isDraftEdit: true, | ||||
| 	}, { | ||||
| 		done: result => { | ||||
| 			if (result.updated) { | ||||
|  | @ -80,16 +81,16 @@ async function undrafted(emoji) { | |||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 
 | ||||
| 	await os.api('admin/emoji/update', { | ||||
| 	await os.api('admin/emoji/draft-update', { | ||||
| 		id: emoji.id, | ||||
| 		fileId: emoji.fileId, | ||||
| 		name: emoji.name, | ||||
| 		category: emoji.category, | ||||
| 		aliases: emoji.aliases, | ||||
| 		license: emoji.license, | ||||
| 		draft: false, | ||||
| 		isSensitive: emoji.isSensitive, | ||||
| 		localOnly: emoji.localOnly, | ||||
| 		roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, | ||||
| 		isDraft: false, | ||||
| 	}); | ||||
| 
 | ||||
| 	emojisDraftPaginationComponent.value.removeItem((item) => item.id === emoji.id); | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 				<MkInput v-model="license"> | ||||
| 					<template #label>{{ i18n.ts.license }}</template> | ||||
| 				</MkInput> | ||||
| 				<MkFolder v-if="!isRequest"> | ||||
| 				<MkFolder v-if="!isRequest && !isDraftEdit"> | ||||
| 					<template #label>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction }}</template> | ||||
| 					<template #suffix>{{ rolesThatCanBeUsedThisEmojiAsReaction.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisEmojiAsReaction.length }}</template> | ||||
| 
 | ||||
|  | @ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 				</MkFolder> | ||||
| 				<MkSwitch v-model="isSensitive">isSensitive</MkSwitch> | ||||
| 				<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch> | ||||
| 				<MkSwitch v-if="!isRequest" v-model="draft" :disabled="isRequest"> | ||||
| 				<MkSwitch v-if="!isRequest" v-model="isDraft" :disabled="isRequest"> | ||||
| 					{{ i18n.ts.draft }} | ||||
| 				</MkSwitch> | ||||
| 			</div> | ||||
|  | @ -100,6 +100,7 @@ import MkRolePreview from '@/components/MkRolePreview.vue'; | |||
| const props = defineProps<{ | ||||
|   emoji?: any, | ||||
|   isRequest: boolean, | ||||
|   isDraftEdit?: boolean, | ||||
| }>(); | ||||
| 
 | ||||
| let dialog = $ref(null); | ||||
|  | @ -109,18 +110,18 @@ let aliases: string = $ref(props.emoji ? props.emoji.aliases.join(' ') : ''); | |||
| let license: string = $ref(props.emoji ? (props.emoji.license ?? '') : ''); | ||||
| let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false); | ||||
| let localOnly = $ref(props.emoji ? props.emoji.localOnly : false); | ||||
| let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); | ||||
| let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref((props.emoji && props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction) ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); | ||||
| let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]); | ||||
| let file = $ref<Misskey.entities.DriveFile>(); | ||||
| let chooseFile: DriveFile|null = $ref(null); | ||||
| let draft = $ref(props.emoji ? props.emoji.draft : false); | ||||
| let isRequest = $ref(props.isRequest); | ||||
| 
 | ||||
| let isDraftEdit = $ref(props.isDraftEdit ?? false); | ||||
| let isDraft = $ref(!!(isDraftEdit || props.isRequest)); | ||||
| watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => { | ||||
| 	rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); | ||||
| }, { immediate: true }); | ||||
| 
 | ||||
| const imgUrl = computed(() => file ? file.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null); | ||||
| const imgUrl = computed(() => file ? file.url : props.emoji && !isDraftEdit ? `/emoji/${props.emoji.name}.webp` : props.emoji && props.emoji.url ? props.emoji.url : null); | ||||
| const validation = computed(() => { | ||||
| 	return name.match(/^[a-zA-Z0-9_]+$/) && imgUrl.value != null; | ||||
| }); | ||||
|  | @ -129,28 +130,6 @@ const emit = defineEmits<{ | |||
|   (ev: 'closed'): void | ||||
| }>(); | ||||
| 
 | ||||
| async function add() { | ||||
| 	const ret = await os.api('admin/emoji/add-draft', { | ||||
| 		name: name, | ||||
| 		category: category, | ||||
| 		aliases: aliases.split(' '), | ||||
| 		license: license === '' ? null : license, | ||||
| 		fileId: chooseFile.id, | ||||
| 	}); | ||||
| 
 | ||||
| 	emit('done', { | ||||
| 		updated: { | ||||
| 			id: ret.id, | ||||
| 			name, | ||||
| 			category, | ||||
| 			aliases: aliases.split(' '), | ||||
| 			license: license === '' ? null : license, | ||||
| 			draft: true, | ||||
| 		}, | ||||
| 	}); | ||||
| 
 | ||||
| 	dialog.close(); | ||||
| } | ||||
| async function changeImage(ev) { | ||||
| 	file = await selectFile(ev.currentTarget ?? ev.target, null); | ||||
| 	const candidate = file.name.replace(/\.(.+)$/, ''); | ||||
|  | @ -174,37 +153,13 @@ async function addRole() { | |||
| async function removeRole(role, ev) { | ||||
| 	rolesThatCanBeUsedThisEmojiAsReaction = rolesThatCanBeUsedThisEmojiAsReaction.filter(x => x.id !== role.id); | ||||
| } | ||||
| async function update() { | ||||
| 	await os.apiWithDialog('admin/emoji/update', { | ||||
| 		id: props.emoji.id, | ||||
| 		name, | ||||
| 		category, | ||||
| 		aliases: aliases.split(' '), | ||||
| 		license: license === '' ? null : license, | ||||
| 		fileId: chooseFile?.id, | ||||
| 		draft: draft, | ||||
| 	}); | ||||
| 
 | ||||
| 	emit('done', { | ||||
| 		updated: { | ||||
| 			id: props.emoji.id, | ||||
| 			name, | ||||
| 			category, | ||||
| 			aliases: aliases.split(' '), | ||||
| 			license: license === '' ? null : license, | ||||
| 			draft: draft, | ||||
| 		}, | ||||
| 	}); | ||||
| 
 | ||||
| 	dialog.close(); | ||||
| } | ||||
| async function done() { | ||||
| 	const params = { | ||||
| 		name, | ||||
| 		category: category === '' ? null : category, | ||||
| 		aliases: aliases.replace(' ', ' ').split(' ').filter(x => x !== ''), | ||||
| 		license: license === '' ? null : license, | ||||
| 		draft: draft, | ||||
| 		draft: isDraft, | ||||
| 		isSensitive, | ||||
| 		localOnly, | ||||
| 		roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id), | ||||
|  | @ -213,12 +168,19 @@ async function done() { | |||
| 	if (file) { | ||||
| 		params.fileId = file.id; | ||||
| 	} | ||||
| 	console.log(props.emoji); | ||||
| 
 | ||||
| 	if (props.emoji) { | ||||
| 		await os.apiWithDialog('admin/emoji/update', { | ||||
| 			id: props.emoji.id, | ||||
| 			...params, | ||||
| 		}); | ||||
| 		if (isDraftEdit) { | ||||
| 			await os.apiWithDialog('admin/emoji/draft-update', { | ||||
| 				id: props.emoji.id, | ||||
| 				...params, | ||||
| 			}); | ||||
| 		} else { | ||||
| 			await os.apiWithDialog('admin/emoji/update', { | ||||
| 				id: props.emoji.id, | ||||
| 				...params, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		emit('done', { | ||||
| 			updated: { | ||||
|  |  | |||
|  | @ -201,11 +201,13 @@ async function init(): Promise<void> { | |||
| 		...params, | ||||
| 		limit: props.pagination.limit ?? 10, | ||||
| 	}).then(res => { | ||||
| 		if (props.pagination.endpoint === 'emoji-drafts') { | ||||
| 			res = res.emojis; | ||||
| 		} | ||||
| 		for (let i = 0; i < res.length; i++) { | ||||
| 			const item = res[i]; | ||||
| 			if (i === 3) item._shouldInsertAd_ = true; | ||||
| 		} | ||||
| 
 | ||||
| 		if (res.length === 0 || props.pagination.noPaging) { | ||||
| 			concatItems(res); | ||||
| 			more.value = false; | ||||
|  | @ -214,7 +216,6 @@ async function init(): Promise<void> { | |||
| 			concatItems(res); | ||||
| 			more.value = true; | ||||
| 		} | ||||
| 
 | ||||
| 		offset.value = res.length; | ||||
| 		error.value = false; | ||||
| 		fetching.value = false; | ||||
|  | @ -241,6 +242,9 @@ const fetchMore = async (): Promise<void> => { | |||
| 			untilId: Array.from(items.value.keys()).at(-1), | ||||
| 		}), | ||||
| 	}).then(res => { | ||||
| 		if (props.pagination.endpoint === 'emoji-drafts') { | ||||
| 			res = res.emojis; | ||||
| 		} | ||||
| 		for (let i = 0; i < res.length; i++) { | ||||
| 			const item = res[i]; | ||||
| 			if (i === 10) item._shouldInsertAd_ = true; | ||||
|  | @ -305,6 +309,9 @@ const fetchMoreAhead = async (): Promise<void> => { | |||
| 			sinceId: Array.from(items.value.keys()).at(-1), | ||||
| 		}), | ||||
| 	}).then(res => { | ||||
| 		if (props.pagination.endpoint === 'emoji-drafts') { | ||||
| 			res = res.emojis; | ||||
| 		} | ||||
| 		if (res.length === 0) { | ||||
| 			items.value = concatMapWithArray(items.value, res); | ||||
| 			more.value = false; | ||||
|  |  | |||
|  | @ -32,13 +32,13 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 		<MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category"> | ||||
| 			<template #header>{{ category || i18n.ts.other }}</template> | ||||
| 			<div :class="$style.emojis"> | ||||
| 				<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category && !e.draft)" :key="emoji.name" :emoji="emoji" :draft="emoji.draft"/> | ||||
| 				<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji"/> | ||||
| 			</div> | ||||
| 		</MkFoldableSection> | ||||
| 	</MkSpacer> | ||||
| 	<MkSpacer v-if="tab === 'draft'" :contentMax="1000" :marginMin="20"> | ||||
| 		<div :class="$style.emojis"> | ||||
| 			<XEmoji v-for="emoji in draftEmojis" :key="emoji.name" :emoji="emoji" :draft="emoji.draft"/> | ||||
| 			<XEmoji v-for="emoji in draftEmojis.emojis" :key="emoji.name" :emoji="emoji" :draft="true"/> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
|  | @ -73,8 +73,7 @@ definePageMetadata(ref({})); | |||
| let q = $ref(''); | ||||
| let searchEmojis = $ref<Misskey.entities.CustomEmoji[]>(null); | ||||
| let selectedTags = $ref(new Set()); | ||||
| const draftEmojis = customEmojis.value.filter(emoji => emoji.draft); | ||||
| 
 | ||||
| const draftEmojis = await os.apiGet('emoji-drafts'); | ||||
| function search() { | ||||
| 	if ((q === '' || q == null) && selectedTags.size === 0) { | ||||
| 		searchEmojis = null; | ||||
|  |  | |||
|  | @ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| --> | ||||
| 
 | ||||
| <template> | ||||
| <button v-if="emoji.draft" class="zuvgdzyu _button emoji-draft" @click="menu"> | ||||
| 	<img :src="emoji.url" class="img" loading="lazy"/> | ||||
| <button v-if="draft" class="_button emoji-draft" :class="$style.root" @click="menu"> | ||||
| 	<img :src="emoji.url" :class="$style.img" loading="lazy"/> | ||||
| 	<div class="body"> | ||||
| 		<div class="name _monospace">{{ emoji.name + ' (draft)' }}</div> | ||||
| 		<div class="info">{{ emoji.aliases.join(' ') }}</div> | ||||
|  | @ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| <button v-else class="_button" :class="$style.root" @click="menu"> | ||||
| 	<img :src="emoji.url" :class="$style.img" loading="lazy"/> | ||||
| 	<div :class="$style.body"> | ||||
| 		<div :class="$style.name" class="_monospace">{{ emoji.name }}</div> | ||||
| 		<div :class="$style.info">{{ emoji.aliases.join(' ') }}</div> | ||||
| 		<div class="name _monospace">{{ emoji.name }}</div> | ||||
| 		<div class="info">{{ emoji.aliases.join(' ') }}</div> | ||||
| 	</div> | ||||
| </button> | ||||
| </template> | ||||
|  | @ -26,13 +26,13 @@ import copyToClipboard from '@/scripts/copy-to-clipboard.js'; | |||
| import { i18n } from '@/i18n.js'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	emoji: { | ||||
| 		name: string; | ||||
| 		aliases: string[]; | ||||
| 		category: string; | ||||
| 		url: string; | ||||
| 		draft: boolean; | ||||
| 	}; | ||||
|   emoji: { | ||||
|     name: string; | ||||
|     aliases: string[]; | ||||
|     category: string; | ||||
|     url: string; | ||||
|   }; | ||||
|   draft?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| function menu(ev) { | ||||
|  | @ -50,7 +50,7 @@ function menu(ev) { | |||
| 		text: i18n.ts.info, | ||||
| 		icon: 'ti ti-info-circle', | ||||
| 		action: () => { | ||||
| 			os.apiGet('emoji', { name: props.emoji.name }).then(res => { | ||||
| 			os.apiGet('emoji-drafts', { name: props.emoji.name }).then(res => { | ||||
| 				os.alert({ | ||||
| 					type: 'info', | ||||
| 					text: `License: ${res.license}`, | ||||
|  | @ -63,45 +63,45 @@ function menu(ev) { | |||
| 
 | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
| 	padding: 12px; | ||||
| 	text-align: left; | ||||
| 	background: var(--panel); | ||||
| 	border-radius: 8px; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   padding: 12px; | ||||
|   text-align: left; | ||||
|   background: var(--panel); | ||||
|   border-radius: 8px; | ||||
| 
 | ||||
| 	&:hover { | ||||
| 		border-color: var(--accent); | ||||
| 	} | ||||
|   &:hover { | ||||
|     border-color: var(--accent); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .img { | ||||
| 	width: 42px; | ||||
| 	height: 42px; | ||||
| 	object-fit: contain; | ||||
|   width: 42px; | ||||
|   height: 42px; | ||||
|   object-fit: contain; | ||||
| } | ||||
| 
 | ||||
| .body { | ||||
| 	padding: 0 0 0 8px; | ||||
| 	white-space: nowrap; | ||||
| 	overflow: hidden; | ||||
|   padding: 0 0 0 8px; | ||||
|   white-space: nowrap; | ||||
|   overflow: hidden; | ||||
| } | ||||
| 
 | ||||
| .name { | ||||
| 	text-overflow: ellipsis; | ||||
| 	overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
|   overflow: hidden; | ||||
| } | ||||
| 
 | ||||
| .info { | ||||
| 	opacity: 0.5; | ||||
| 	font-size: 0.9em; | ||||
| 	text-overflow: ellipsis; | ||||
| 	overflow: hidden; | ||||
|   opacity: 0.5; | ||||
|   font-size: 0.9em; | ||||
|   text-overflow: ellipsis; | ||||
|   overflow: hidden; | ||||
| } | ||||
| 
 | ||||
| .emoji-draft { | ||||
| 	--c: rgb(255 196 0 / 15%);; | ||||
| 	background-image: linear-gradient(45deg,var(--c) 16.67%,transparent 16.67%,transparent 50%,var(--c) 50%,var(--c) 66.67%,transparent 66.67%,transparent 100%); | ||||
| 	background-size: 16px 16px; | ||||
|   --c: rgb(255 196 0 / 15%);; | ||||
|   background-image: linear-gradient(45deg,var(--c) 16.67%,transparent 16.67%,transparent 50%,var(--c) 50%,var(--c) 66.67%,transparent 66.67%,transparent 100%); | ||||
|   background-size: 16px 16px; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue