カスタム絵文字一覧情報をmetaから分離
This commit is contained in:
		
							parent
							
								
									e4144a17a4
								
							
						
					
					
						commit
						462acc9eee
					
				
					 17 changed files with 212 additions and 151 deletions
				
			
		|  | @ -40,6 +40,8 @@ You should also include the user name that made the change. | ||||||
| - Firefox109以下はサポートされなくなりました | - Firefox109以下はサポートされなくなりました | ||||||
| 
 | 
 | ||||||
| #### For app developers | #### For app developers | ||||||
|  | - API: metaのレスポンスに`emojis`プロパティが含まれなくなりました | ||||||
|  | 	- カスタム絵文字一覧情報を取得するには、`emojis`エンドポイントにリクエストします | ||||||
| - API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました | - API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました | ||||||
| 	- 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。 | 	- 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。 | ||||||
| 	- e.g. `https://p1.a9z.dev/emoji/misskey.webp` | 	- e.g. `https://p1.a9z.dev/emoji/misskey.webp` | ||||||
|  |  | ||||||
|  | @ -22,23 +22,25 @@ export class EmojiEntityService { | ||||||
| 	@bindThis | 	@bindThis | ||||||
| 	public async pack( | 	public async pack( | ||||||
| 		src: Emoji['id'] | Emoji, | 		src: Emoji['id'] | Emoji, | ||||||
|  | 		opts: { omitHost?: boolean; omitId?: boolean; } = {}, | ||||||
| 	): Promise<Packed<'Emoji'>> { | 	): Promise<Packed<'Emoji'>> { | ||||||
| 		const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); | 		const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); | ||||||
| 
 | 
 | ||||||
| 		return { | 		return { | ||||||
| 			id: emoji.id, | 			id: opts.omitId ? undefined : emoji.id, | ||||||
| 			aliases: emoji.aliases, | 			aliases: emoji.aliases, | ||||||
| 			name: emoji.name, | 			name: emoji.name, | ||||||
| 			category: emoji.category, | 			category: emoji.category, | ||||||
| 			host: emoji.host, | 			host: opts.omitHost ? undefined : emoji.host, | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@bindThis | 	@bindThis | ||||||
| 	public packMany( | 	public packMany( | ||||||
| 		emojis: any[], | 		emojis: any[], | ||||||
|  | 		opts: { omitHost?: boolean; omitId?: boolean; } = {}, | ||||||
| 	) { | 	) { | ||||||
| 		return Promise.all(emojis.map(x => this.pack(x))); | 		return Promise.all(emojis.map(x => this.pack(x, opts))); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ export const packedEmojiSchema = { | ||||||
| 	properties: { | 	properties: { | ||||||
| 		id: { | 		id: { | ||||||
| 			type: 'string', | 			type: 'string', | ||||||
| 			optional: false, nullable: false, | 			optional: true, nullable: false, | ||||||
| 			format: 'id', | 			format: 'id', | ||||||
| 			example: 'xxxxxxxxxx', | 			example: 'xxxxxxxxxx', | ||||||
| 		}, | 		}, | ||||||
|  | @ -26,12 +26,8 @@ export const packedEmojiSchema = { | ||||||
| 		}, | 		}, | ||||||
| 		host: { | 		host: { | ||||||
| 			type: 'string', | 			type: 'string', | ||||||
| 			optional: false, nullable: true, | 			optional: true, nullable: true, | ||||||
| 			description: 'The local host is represented with `null`.', | 			description: 'The local host is represented with `null`.', | ||||||
| 		}, | 		}, | ||||||
| 		url: { |  | ||||||
| 			type: 'string', |  | ||||||
| 			optional: true, nullable: false, |  | ||||||
| 		}, |  | ||||||
| 	}, | 	}, | ||||||
| } as const; | } as const; | ||||||
|  |  | ||||||
|  | @ -220,6 +220,7 @@ import * as ep___messaging_messages_create from './endpoints/messaging/messages/ | ||||||
| import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js'; | import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js'; | ||||||
| import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; | import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; | ||||||
| import * as ep___meta from './endpoints/meta.js'; | import * as ep___meta from './endpoints/meta.js'; | ||||||
|  | import * as ep___emojis from './endpoints/emojis.js'; | ||||||
| import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; | import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; | ||||||
| import * as ep___mute_create from './endpoints/mute/create.js'; | import * as ep___mute_create from './endpoints/mute/create.js'; | ||||||
| import * as ep___mute_delete from './endpoints/mute/delete.js'; | import * as ep___mute_delete from './endpoints/mute/delete.js'; | ||||||
|  | @ -550,6 +551,7 @@ const $messaging_messages_create: Provider = { provide: 'ep:messaging/messages/c | ||||||
| const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default }; | const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default }; | ||||||
| const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default }; | const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default }; | ||||||
| const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; | const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; | ||||||
|  | const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default }; | ||||||
| const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.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 }; | const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default }; | ||||||
| const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default }; | const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default }; | ||||||
|  | @ -884,6 +886,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention | ||||||
| 		$messaging_messages_delete, | 		$messaging_messages_delete, | ||||||
| 		$messaging_messages_read, | 		$messaging_messages_read, | ||||||
| 		$meta, | 		$meta, | ||||||
|  | 		$emojis, | ||||||
| 		$miauth_genToken, | 		$miauth_genToken, | ||||||
| 		$mute_create, | 		$mute_create, | ||||||
| 		$mute_delete, | 		$mute_delete, | ||||||
|  | @ -1212,6 +1215,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention | ||||||
| 		$messaging_messages_delete, | 		$messaging_messages_delete, | ||||||
| 		$messaging_messages_read, | 		$messaging_messages_read, | ||||||
| 		$meta, | 		$meta, | ||||||
|  | 		$emojis, | ||||||
| 		$miauth_genToken, | 		$miauth_genToken, | ||||||
| 		$mute_create, | 		$mute_create, | ||||||
| 		$mute_delete, | 		$mute_delete, | ||||||
|  |  | ||||||
|  | @ -219,6 +219,7 @@ import * as ep___messaging_messages_create from './endpoints/messaging/messages/ | ||||||
| import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js'; | import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js'; | ||||||
| import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; | import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; | ||||||
| import * as ep___meta from './endpoints/meta.js'; | import * as ep___meta from './endpoints/meta.js'; | ||||||
|  | import * as ep___emojis from './endpoints/emojis.js'; | ||||||
| import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; | import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; | ||||||
| import * as ep___mute_create from './endpoints/mute/create.js'; | import * as ep___mute_create from './endpoints/mute/create.js'; | ||||||
| import * as ep___mute_delete from './endpoints/mute/delete.js'; | import * as ep___mute_delete from './endpoints/mute/delete.js'; | ||||||
|  | @ -547,6 +548,7 @@ const eps = [ | ||||||
| 	['messaging/messages/delete', ep___messaging_messages_delete], | 	['messaging/messages/delete', ep___messaging_messages_delete], | ||||||
| 	['messaging/messages/read', ep___messaging_messages_read], | 	['messaging/messages/read', ep___messaging_messages_read], | ||||||
| 	['meta', ep___meta], | 	['meta', ep___meta], | ||||||
|  | 	['emojis', ep___emojis], | ||||||
| 	['miauth/gen-token', ep___miauth_genToken], | 	['miauth/gen-token', ep___miauth_genToken], | ||||||
| 	['mute/create', ep___mute_create], | 	['mute/create', ep___mute_create], | ||||||
| 	['mute/delete', ep___mute_delete], | 	['mute/delete', ep___mute_delete], | ||||||
|  |  | ||||||
							
								
								
									
										91
									
								
								packages/backend/src/server/api/endpoints/emojis.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								packages/backend/src/server/api/endpoints/emojis.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,91 @@ | ||||||
|  | import { IsNull, MoreThan } from 'typeorm'; | ||||||
|  | import { Inject, Injectable } from '@nestjs/common'; | ||||||
|  | import type { EmojisRepository } from '@/models/index.js'; | ||||||
|  | import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||||
|  | import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; | ||||||
|  | import type { Config } from '@/config.js'; | ||||||
|  | import { DI } from '@/di-symbols.js'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['meta'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: false, | ||||||
|  | 
 | ||||||
|  | 	res: { | ||||||
|  | 		type: 'object', | ||||||
|  | 		optional: false, nullable: false, | ||||||
|  | 		properties: { | ||||||
|  | 			emojis: { | ||||||
|  | 				type: 'array', | ||||||
|  | 				optional: false, nullable: false, | ||||||
|  | 				items: { | ||||||
|  | 					type: 'object', | ||||||
|  | 					optional: false, nullable: false, | ||||||
|  | 					properties: { | ||||||
|  | 						id: { | ||||||
|  | 							type: 'string', | ||||||
|  | 							optional: false, nullable: false, | ||||||
|  | 							format: 'id', | ||||||
|  | 						}, | ||||||
|  | 						aliases: { | ||||||
|  | 							type: 'array', | ||||||
|  | 							optional: false, nullable: false, | ||||||
|  | 							items: { | ||||||
|  | 								type: 'string', | ||||||
|  | 								optional: false, nullable: false, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 						category: { | ||||||
|  | 							type: 'string', | ||||||
|  | 							optional: false, nullable: true, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 	}, | ||||||
|  | 	required: [], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | // eslint-disable-next-line import/no-default-export
 | ||||||
|  | @Injectable() | ||||||
|  | export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||||
|  | 	constructor( | ||||||
|  | 		@Inject(DI.config) | ||||||
|  | 		private config: Config, | ||||||
|  | 	 | ||||||
|  | 		@Inject(DI.emojisRepository) | ||||||
|  | 		private emojisRepository: EmojisRepository, | ||||||
|  | 
 | ||||||
|  | 		private emojiEntityService: EmojiEntityService, | ||||||
|  | 	) { | ||||||
|  | 		super(meta, paramDef, async (ps, me) => { | ||||||
|  | 			const emojis = await this.emojisRepository.find({ | ||||||
|  | 				where: { | ||||||
|  | 					host: IsNull(), | ||||||
|  | 				}, | ||||||
|  | 				order: { | ||||||
|  | 					category: 'ASC', | ||||||
|  | 					name: 'ASC', | ||||||
|  | 				}, | ||||||
|  | 				cache: { | ||||||
|  | 					id: 'meta_emojis', | ||||||
|  | 					milliseconds: 3600000,	// 1 hour
 | ||||||
|  | 				}, | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			return { | ||||||
|  | 				emojis: await this.emojiEntityService.packMany(emojis, { | ||||||
|  | 					omitId: true, | ||||||
|  | 					omitHost: true, | ||||||
|  | 				}), | ||||||
|  | 			}; | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -4,7 +4,6 @@ import type { AdsRepository, EmojisRepository, UsersRepository } from '@/models/ | ||||||
| import { MAX_NOTE_TEXT_LENGTH, DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; | import { MAX_NOTE_TEXT_LENGTH, DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; | ||||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||||
| import { UserEntityService } from '@/core/entities/UserEntityService.js'; | import { UserEntityService } from '@/core/entities/UserEntityService.js'; | ||||||
| import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; |  | ||||||
| import { MetaService } from '@/core/MetaService.js'; | import { MetaService } from '@/core/MetaService.js'; | ||||||
| import type { Config } from '@/config.js'; | import type { Config } from '@/config.js'; | ||||||
| import { DI } from '@/di-symbols.js'; | import { DI } from '@/di-symbols.js'; | ||||||
|  | @ -152,43 +151,6 @@ export const meta = { | ||||||
| 				type: 'number', | 				type: 'number', | ||||||
| 				optional: false, nullable: false, | 				optional: false, nullable: false, | ||||||
| 			}, | 			}, | ||||||
| 			emojis: { |  | ||||||
| 				type: 'array', |  | ||||||
| 				optional: false, nullable: false, |  | ||||||
| 				items: { |  | ||||||
| 					type: 'object', |  | ||||||
| 					optional: false, nullable: false, |  | ||||||
| 					properties: { |  | ||||||
| 						id: { |  | ||||||
| 							type: 'string', |  | ||||||
| 							optional: false, nullable: false, |  | ||||||
| 							format: 'id', |  | ||||||
| 						}, |  | ||||||
| 						aliases: { |  | ||||||
| 							type: 'array', |  | ||||||
| 							optional: false, nullable: false, |  | ||||||
| 							items: { |  | ||||||
| 								type: 'string', |  | ||||||
| 								optional: false, nullable: false, |  | ||||||
| 							}, |  | ||||||
| 						}, |  | ||||||
| 						category: { |  | ||||||
| 							type: 'string', |  | ||||||
| 							optional: false, nullable: true, |  | ||||||
| 						}, |  | ||||||
| 						host: { |  | ||||||
| 							type: 'string', |  | ||||||
| 							optional: false, nullable: true, |  | ||||||
| 							description: 'The local host is represented with `null`.', |  | ||||||
| 						}, |  | ||||||
| 						url: { |  | ||||||
| 							type: 'string', |  | ||||||
| 							optional: false, nullable: false, |  | ||||||
| 							format: 'url', |  | ||||||
| 						}, |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			ads: { | 			ads: { | ||||||
| 				type: 'array', | 				type: 'array', | ||||||
| 				optional: false, nullable: false, | 				optional: false, nullable: false, | ||||||
|  | @ -326,30 +288,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||||
| 		@Inject(DI.adsRepository) | 		@Inject(DI.adsRepository) | ||||||
| 		private adsRepository: AdsRepository, | 		private adsRepository: AdsRepository, | ||||||
| 
 | 
 | ||||||
| 		@Inject(DI.emojisRepository) |  | ||||||
| 		private emojisRepository: EmojisRepository, |  | ||||||
| 
 |  | ||||||
| 		private userEntityService: UserEntityService, | 		private userEntityService: UserEntityService, | ||||||
| 		private emojiEntityService: EmojiEntityService, |  | ||||||
| 		private metaService: MetaService, | 		private metaService: MetaService, | ||||||
| 	) { | 	) { | ||||||
| 		super(meta, paramDef, async (ps, me) => { | 		super(meta, paramDef, async (ps, me) => { | ||||||
| 			const instance = await this.metaService.fetch(true); | 			const instance = await this.metaService.fetch(true); | ||||||
| 
 | 
 | ||||||
| 			const emojis = await this.emojisRepository.find({ |  | ||||||
| 				where: { |  | ||||||
| 					host: IsNull(), |  | ||||||
| 				}, |  | ||||||
| 				order: { |  | ||||||
| 					category: 'ASC', |  | ||||||
| 					name: 'ASC', |  | ||||||
| 				}, |  | ||||||
| 				cache: { |  | ||||||
| 					id: 'meta_emojis', |  | ||||||
| 					milliseconds: 3600000,	// 1 hour
 |  | ||||||
| 				}, |  | ||||||
| 			}); |  | ||||||
| 
 |  | ||||||
| 			const ads = await this.adsRepository.find({ | 			const ads = await this.adsRepository.find({ | ||||||
| 				where: { | 				where: { | ||||||
| 					expiresAt: MoreThan(new Date()), | 					expiresAt: MoreThan(new Date()), | ||||||
|  | @ -390,7 +334,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||||
| 				backgroundImageUrl: instance.backgroundImageUrl, | 				backgroundImageUrl: instance.backgroundImageUrl, | ||||||
| 				logoImageUrl: instance.logoImageUrl, | 				logoImageUrl: instance.logoImageUrl, | ||||||
| 				maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
 | 				maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
 | ||||||
| 				emojis: await this.emojiEntityService.packMany(emojis), |  | ||||||
| 				defaultLightTheme: instance.defaultLightTheme, | 				defaultLightTheme: instance.defaultLightTheme, | ||||||
| 				defaultDarkTheme: instance.defaultDarkTheme, | 				defaultDarkTheme: instance.defaultDarkTheme, | ||||||
| 				ads: ads.map(ad => ({ | 				ads: ads.map(ad => ({ | ||||||
|  |  | ||||||
|  | @ -47,6 +47,9 @@ import { emojilist } from '@/scripts/emojilist'; | ||||||
| import { instance } from '@/instance'; | import { instance } from '@/instance'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { miLocalStorage } from '@/local-storage'; | import { miLocalStorage } from '@/local-storage'; | ||||||
|  | import { getCustomEmojis } from '@/custom-emojis'; | ||||||
|  | 
 | ||||||
|  | const customEmojis = await getCustomEmojis(); | ||||||
| 
 | 
 | ||||||
| type EmojiDef = { | type EmojiDef = { | ||||||
| 	emoji: string; | 	emoji: string; | ||||||
|  | @ -86,7 +89,6 @@ for (const x of lib) { | ||||||
| emjdb.sort((a, b) => a.name.length - b.name.length); | emjdb.sort((a, b) => a.name.length - b.name.length); | ||||||
| 
 | 
 | ||||||
| //#region Construct Emoji DB | //#region Construct Emoji DB | ||||||
| const customEmojis = instance.emojis; |  | ||||||
| const emojiDefinitions: EmojiDef[] = []; | const emojiDefinitions: EmojiDef[] = []; | ||||||
| 
 | 
 | ||||||
| for (const x of customEmojis) { | for (const x of customEmojis) { | ||||||
|  | @ -117,7 +119,6 @@ export default { | ||||||
| 	emojiDb, | 	emojiDb, | ||||||
| 	emojiDefinitions, | 	emojiDefinitions, | ||||||
| 	emojilist, | 	emojilist, | ||||||
| 	customEmojis, |  | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
| 			<div v-if="searchResultCustom.length > 0" class="body"> | 			<div v-if="searchResultCustom.length > 0" class="body"> | ||||||
| 				<button | 				<button | ||||||
| 					v-for="emoji in searchResultCustom" | 					v-for="emoji in searchResultCustom" | ||||||
| 					:key="emoji.id" | 					:key="emoji.name" | ||||||
| 					class="_button item" | 					class="_button item" | ||||||
| 					:title="emoji.name" | 					:title="emoji.name" | ||||||
| 					tabindex="0" | 					tabindex="0" | ||||||
|  | @ -85,9 +85,10 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { isTouchUsing } from '@/scripts/touch'; | import { isTouchUsing } from '@/scripts/touch'; | ||||||
| import { deviceKind } from '@/scripts/device-kind'; | import { deviceKind } from '@/scripts/device-kind'; | ||||||
| import { emojiCategories, instance } from '@/instance'; | import { instance } from '@/instance'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
|  | import { getCustomEmojiCategories, getCustomEmojis } from '@/custom-emojis'; | ||||||
| 
 | 
 | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	showPinned?: boolean; | 	showPinned?: boolean; | ||||||
|  | @ -103,6 +104,7 @@ const emit = defineEmits<{ | ||||||
| 	(ev: 'chosen', v: string): void; | 	(ev: 'chosen', v: string): void; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
|  | const customEmojis = await getCustomEmojis(); | ||||||
| const search = shallowRef<HTMLInputElement>(); | const search = shallowRef<HTMLInputElement>(); | ||||||
| const emojis = shallowRef<HTMLDivElement>(); | const emojis = shallowRef<HTMLDivElement>(); | ||||||
| 
 | 
 | ||||||
|  | @ -118,8 +120,7 @@ const { | ||||||
| const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1); | const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1); | ||||||
| const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3); | const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3); | ||||||
| const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2); | const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2); | ||||||
| const customEmojiCategories = emojiCategories; | const customEmojiCategories = await getCustomEmojiCategories(); | ||||||
| const customEmojis = instance.emojis; |  | ||||||
| const q = ref<string>(''); | const q = ref<string>(''); | ||||||
| const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]); | const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]); | ||||||
| const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); | const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); | ||||||
|  |  | ||||||
							
								
								
									
										48
									
								
								packages/frontend/src/custom-emojis.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								packages/frontend/src/custom-emojis.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | ||||||
|  | import { api } from './os'; | ||||||
|  | import { miLocalStorage } from './local-storage'; | ||||||
|  | 
 | ||||||
|  | const storageCache = miLocalStorage.getItem('emojis'); | ||||||
|  | let cached = storageCache ? JSON.parse(storageCache) : null; | ||||||
|  | export async function getCustomEmojis() { | ||||||
|  | 	const now = Date.now(); | ||||||
|  | 	const lastFetchedAt = miLocalStorage.getItem('lastEmojisFetchedAt'); | ||||||
|  | 	if (cached && lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60 * 60) return cached; | ||||||
|  | 
 | ||||||
|  | 	const res = await api('emojis', {}); | ||||||
|  | 
 | ||||||
|  | 	cached = res.emojis; | ||||||
|  | 	miLocalStorage.setItem('emojis', JSON.stringify(cached)); | ||||||
|  | 	miLocalStorage.setItem('lastEmojisFetchedAt', now.toString()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let cachedCategories; | ||||||
|  | export async function getCustomEmojiCategories() { | ||||||
|  | 	if (cachedCategories) return cachedCategories; | ||||||
|  | 
 | ||||||
|  | 	const customEmojis = await getCustomEmojis(); | ||||||
|  | 
 | ||||||
|  | 	const categories = new Set(); | ||||||
|  | 	for (const emoji of customEmojis) { | ||||||
|  | 		categories.add(emoji.category); | ||||||
|  | 	} | ||||||
|  | 	const res = Array.from(categories); | ||||||
|  | 	cachedCategories = res; | ||||||
|  | 	return res; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let cachedTags; | ||||||
|  | export async function getCustomEmojiTags() { | ||||||
|  | 	if (cachedTags) return cachedTags; | ||||||
|  | 
 | ||||||
|  | 	const customEmojis = await getCustomEmojis(); | ||||||
|  | 
 | ||||||
|  | 	const tags = new Set(); | ||||||
|  | 	for (const emoji of customEmojis) { | ||||||
|  | 		for (const tag of emoji.aliases) { | ||||||
|  | 			tags.add(tag); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	const res = Array.from(tags); | ||||||
|  | 	cachedTags = res; | ||||||
|  | 	return res; | ||||||
|  | } | ||||||
|  | @ -5,11 +5,11 @@ import { miLocalStorage } from './local-storage'; | ||||||
| 
 | 
 | ||||||
| // TODO: 他のタブと永続化されたstateを同期
 | // TODO: 他のタブと永続化されたstateを同期
 | ||||||
| 
 | 
 | ||||||
| const instanceData = miLocalStorage.getItem('instance'); | const cached = miLocalStorage.getItem('instance'); | ||||||
| 
 | 
 | ||||||
| // TODO: instanceをリアクティブにするかは再考の余地あり
 | // TODO: instanceをリアクティブにするかは再考の余地あり
 | ||||||
| 
 | 
 | ||||||
| export const instance: Misskey.entities.InstanceMetadata = reactive(instanceData ? JSON.parse(instanceData) : { | export const instance: Misskey.entities.InstanceMetadata = reactive(cached ? JSON.parse(cached) : { | ||||||
| 	// TODO: set default values
 | 	// TODO: set default values
 | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | @ -24,23 +24,3 @@ export async function fetchInstance() { | ||||||
| 
 | 
 | ||||||
| 	miLocalStorage.setItem('instance', JSON.stringify(instance)); | 	miLocalStorage.setItem('instance', JSON.stringify(instance)); | ||||||
| } | } | ||||||
| 
 |  | ||||||
| export const emojiCategories = computed(() => { |  | ||||||
| 	if (instance.emojis == null) return []; |  | ||||||
| 	const categories = new Set(); |  | ||||||
| 	for (const emoji of instance.emojis) { |  | ||||||
| 		categories.add(emoji.category); |  | ||||||
| 	} |  | ||||||
| 	return Array.from(categories); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export const emojiTags = computed(() => { |  | ||||||
| 	if (instance.emojis == null) return []; |  | ||||||
| 	const tags = new Set(); |  | ||||||
| 	for (const emoji of instance.emojis) { |  | ||||||
| 		for (const tag of emoji.aliases) { |  | ||||||
| 			tags.add(tag); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return Array.from(tags); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
|  | @ -2,6 +2,8 @@ type Keys = | ||||||
| 	'v' | | 	'v' | | ||||||
| 	'lastVersion' | | 	'lastVersion' | | ||||||
| 	'instance' | | 	'instance' | | ||||||
|  | 	'emojis' | // TODO: indexed db
 | ||||||
|  | 	'lastEmojisFetchedAt' | | ||||||
| 	'account' | | 	'account' | | ||||||
| 	'accounts' | | 	'accounts' | | ||||||
| 	'latestDonationInfoShownAt' | | 	'latestDonationInfoShownAt' | | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ | ||||||
| 
 | 
 | ||||||
| 		<!-- たくさんあると邪魔 | 		<!-- たくさんあると邪魔 | ||||||
| 		<div class="tags"> | 		<div class="tags"> | ||||||
| 			<span class="tag _button" v-for="tag in tags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span> | 			<span class="tag _button" v-for="tag in customEmojiTags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span> | ||||||
| 		</div> | 		</div> | ||||||
| 		--> | 		--> | ||||||
| 	</div> | 	</div> | ||||||
|  | @ -28,8 +28,8 @@ | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent, computed } from 'vue'; | import { defineComponent, computed, watch } from 'vue'; | ||||||
| import XEmoji from './emojis.emoji.vue'; | import XEmoji from './emojis.emoji.vue'; | ||||||
| import MkButton from '@/components/MkButton.vue'; | import MkButton from '@/components/MkButton.vue'; | ||||||
| import MkInput from '@/components/MkInput.vue'; | import MkInput from '@/components/MkInput.vue'; | ||||||
|  | @ -37,62 +37,43 @@ import MkSelect from '@/components/MkSelect.vue'; | ||||||
| import MkFoldableSection from '@/components/MkFoldableSection.vue'; | import MkFoldableSection from '@/components/MkFoldableSection.vue'; | ||||||
| import MkTab from '@/components/MkTab.vue'; | import MkTab from '@/components/MkTab.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { emojiCategories, emojiTags } from '@/instance'; | import { getCustomEmojis, getCustomEmojiCategories, getCustomEmojiTags } from '@/custom-emojis'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const customEmojis = await getCustomEmojis(); | ||||||
| 	components: { | const customEmojiCategories = await getCustomEmojiCategories(); | ||||||
| 		MkButton, | const customEmojiTags = await getCustomEmojiTags(); | ||||||
| 		MkInput, | let q = $ref(''); | ||||||
| 		MkSelect, | let searchEmojis = $ref(null); | ||||||
| 		MkFoldableSection, | let selectedTags = $ref(new Set()); | ||||||
| 		MkTab, |  | ||||||
| 		XEmoji, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | function search() { | ||||||
| 		return { | 	if ((q === '' || q == null) && selectedTags.size === 0) { | ||||||
| 			q: '', | 		searchEmojis = null; | ||||||
| 			customEmojiCategories: emojiCategories, |  | ||||||
| 			customEmojis: this.$instance.emojis, |  | ||||||
| 			tags: emojiTags, |  | ||||||
| 			selectedTags: new Set(), |  | ||||||
| 			searchEmojis: null, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	watch: { |  | ||||||
| 		q() { this.search(); }, |  | ||||||
| 		selectedTags: { |  | ||||||
| 			handler() { |  | ||||||
| 				this.search(); |  | ||||||
| 			}, |  | ||||||
| 			deep: true, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		search() { |  | ||||||
| 			if ((this.q === '' || this.q == null) && this.selectedTags.size === 0) { |  | ||||||
| 				this.searchEmojis = null; |  | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 			if (this.selectedTags.size === 0) { | 	if (selectedTags.size === 0) { | ||||||
| 				this.searchEmojis = this.customEmojis.filter(emoji => emoji.name.includes(this.q) || emoji.aliases.includes(this.q)); | 		searchEmojis = customEmojis.filter(emoji => emoji.name.includes(q) || emoji.aliases.includes(q)); | ||||||
| 	} else { | 	} else { | ||||||
| 				this.searchEmojis = this.customEmojis.filter(emoji => (emoji.name.includes(this.q) || emoji.aliases.includes(this.q)) && [...this.selectedTags].every(t => emoji.aliases.includes(t))); | 		searchEmojis = customEmojis.filter(emoji => (emoji.name.includes(q) || emoji.aliases.includes(q)) && [...selectedTags].every(t => emoji.aliases.includes(t))); | ||||||
| 	} | 	} | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		toggleTag(tag) { | function toggleTag(tag) { | ||||||
| 			if (this.selectedTags.has(tag)) { | 	if (selectedTags.has(tag)) { | ||||||
| 				this.selectedTags.delete(tag); | 		selectedTags.delete(tag); | ||||||
| 	} else { | 	} else { | ||||||
| 				this.selectedTags.add(tag); | 		selectedTags.add(tag); | ||||||
| 	} | 	} | ||||||
| 		}, | } | ||||||
| 	}, | 
 | ||||||
|  | watch($$(q), () => { | ||||||
|  | 	search(); | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | watch($$(selectedTags), () => { | ||||||
|  | 	search(); | ||||||
|  | }, { deep: true }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ import MkInput from '@/components/MkInput.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { unique } from '@/scripts/array'; | import { unique } from '@/scripts/array'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { emojiCategories } from '@/instance'; | import { getCustomEmojiCategories } from '@/custom-emojis'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	emoji: any, | 	emoji: any, | ||||||
|  | @ -46,7 +46,7 @@ let dialog = $ref(null); | ||||||
| let name: string = $ref(props.emoji.name); | let name: string = $ref(props.emoji.name); | ||||||
| let category: string = $ref(props.emoji.category); | let category: string = $ref(props.emoji.category); | ||||||
| let aliases: string = $ref(props.emoji.aliases.join(' ')); | let aliases: string = $ref(props.emoji.aliases.join(' ')); | ||||||
| let categories: string[] = $ref(emojiCategories); | const categories = await getCustomEmojiCategories(); | ||||||
| 
 | 
 | ||||||
| const emit = defineEmits<{ | const emit = defineEmits<{ | ||||||
| 	(ev: 'done', v: { deleted?: boolean, updated?: any }): void, | 	(ev: 'done', v: { deleted?: boolean, updated?: any }): void, | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ | ||||||
| 				<div class="icon"><i class="ti ti-icons"></i></div> | 				<div class="icon"><i class="ti ti-icons"></i></div> | ||||||
| 				<div class="body"> | 				<div class="body"> | ||||||
| 					<div class="value"> | 					<div class="value"> | ||||||
| 						<MkNumber :value="$instance.emojis.length" style="margin-right: 0.5em;"/> | 						<MkNumber :value="customEmojis.length" style="margin-right: 0.5em;"/> | ||||||
| 					</div> | 					</div> | ||||||
| 					<div class="label">Custom emojis</div> | 					<div class="label">Custom emojis</div> | ||||||
| 				</div> | 				</div> | ||||||
|  | @ -63,6 +63,9 @@ import number from '@/filters/number'; | ||||||
| import MkNumberDiff from '@/components/MkNumberDiff.vue'; | import MkNumberDiff from '@/components/MkNumberDiff.vue'; | ||||||
| import MkNumber from '@/components/MkNumber.vue'; | import MkNumber from '@/components/MkNumber.vue'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { getCustomEmojis } from '@/custom-emojis'; | ||||||
|  | 
 | ||||||
|  | const customEmojis = await getCustomEmojis(); | ||||||
| 
 | 
 | ||||||
| let stats: any = $ref(null); | let stats: any = $ref(null); | ||||||
| let usersComparedToThePrevDay = $ref<number>(); | let usersComparedToThePrevDay = $ref<number>(); | ||||||
|  |  | ||||||
|  | @ -317,12 +317,15 @@ import MkTextarea from '@/components/MkTextarea.vue'; | ||||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { instance } from '@/instance'; | import { instance } from '@/instance'; | ||||||
|  | import { getCustomEmojis } from '@/custom-emojis'; | ||||||
|  | 
 | ||||||
|  | const customEmojis = await getCustomEmojis(); | ||||||
| 
 | 
 | ||||||
| let preview_mention = $ref('@example'); | let preview_mention = $ref('@example'); | ||||||
| let preview_hashtag = $ref('#test'); | let preview_hashtag = $ref('#test'); | ||||||
| let preview_url = $ref('https://example.com'); | let preview_url = $ref('https://example.com'); | ||||||
| let preview_link = $ref(`[${i18n.ts._mfm.dummy}](https://example.com)`); | let preview_link = $ref(`[${i18n.ts._mfm.dummy}](https://example.com)`); | ||||||
| let preview_emoji = $ref(instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:'); | let preview_emoji = $ref(customEmojis.length ? `:${customEmojis[0].name}:` : ':emojiname:'); | ||||||
| let preview_bold = $ref(`**${i18n.ts._mfm.dummy}**`); | let preview_bold = $ref(`**${i18n.ts._mfm.dummy}**`); | ||||||
| let preview_small = $ref(`<small>${i18n.ts._mfm.dummy}</small>`); | let preview_small = $ref(`<small>${i18n.ts._mfm.dummy}</small>`); | ||||||
| let preview_center = $ref(`<center>${i18n.ts._mfm.dummy}</center>`); | let preview_center = $ref(`<center>${i18n.ts._mfm.dummy}</center>`); | ||||||
|  |  | ||||||
|  | @ -183,6 +183,8 @@ const menuDef = computed(() => [{ | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			miLocalStorage.removeItem('locale'); | 			miLocalStorage.removeItem('locale'); | ||||||
| 			miLocalStorage.removeItem('theme'); | 			miLocalStorage.removeItem('theme'); | ||||||
|  | 			miLocalStorage.removeItem('emojis'); | ||||||
|  | 			miLocalStorage.removeItem('lastEmojisFetchedAt'); | ||||||
| 			unisonReload(); | 			unisonReload(); | ||||||
| 		}, | 		}, | ||||||
| 	}, { | 	}, { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue