refactor: 絵文字URLを引き回すのをやめる (#9423)
This commit is contained in:
		
							parent
							
								
									510e6ec7e9
								
							
						
					
					
						commit
						912791b3ab
					
				
					 28 changed files with 79 additions and 58 deletions
				
			
		|  | @ -6,8 +6,8 @@ import type { Packed } from '@/misc/schema.js'; | ||||||
| import type { } from '@/models/entities/Blocking.js'; | import type { } from '@/models/entities/Blocking.js'; | ||||||
| import type { User } from '@/models/entities/User.js'; | import type { User } from '@/models/entities/User.js'; | ||||||
| import type { Emoji } from '@/models/entities/Emoji.js'; | import type { Emoji } from '@/models/entities/Emoji.js'; | ||||||
| import { UserEntityService } from './UserEntityService.js'; |  | ||||||
| import { bindThis } from '@/decorators.js'; | import { bindThis } from '@/decorators.js'; | ||||||
|  | import { UserEntityService } from './UserEntityService.js'; | ||||||
| 
 | 
 | ||||||
| @Injectable() | @Injectable() | ||||||
| export class EmojiEntityService { | export class EmojiEntityService { | ||||||
|  | @ -22,6 +22,7 @@ export class EmojiEntityService { | ||||||
| 	@bindThis | 	@bindThis | ||||||
| 	public async pack( | 	public async pack( | ||||||
| 		src: Emoji['id'] | Emoji, | 		src: Emoji['id'] | Emoji, | ||||||
|  | 		opts: { omitUrl?: 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 }); | ||||||
| 
 | 
 | ||||||
|  | @ -32,15 +33,16 @@ export class EmojiEntityService { | ||||||
| 			category: emoji.category, | 			category: emoji.category, | ||||||
| 			host: emoji.host, | 			host: emoji.host, | ||||||
| 			// ?? emoji.originalUrl してるのは後方互換性のため
 | 			// ?? emoji.originalUrl してるのは後方互換性のため
 | ||||||
| 			url: emoji.publicUrl ?? emoji.originalUrl, | 			url: opts.omitUrl ? undefined : (emoji.publicUrl ?? emoji.originalUrl), | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@bindThis | 	@bindThis | ||||||
| 	public packMany( | 	public packMany( | ||||||
| 		emojis: any[], | 		emojis: any[], | ||||||
|  | 		opts: { omitUrl?: boolean } = {}, | ||||||
| 	) { | 	) { | ||||||
| 		return Promise.all(emojis.map(x => this.pack(x))); | 		return Promise.all(emojis.map(x => this.pack(x, opts))); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -31,7 +31,7 @@ export const packedEmojiSchema = { | ||||||
| 		}, | 		}, | ||||||
| 		url: { | 		url: { | ||||||
| 			type: 'string', | 			type: 'string', | ||||||
| 			optional: false, nullable: false, | 			optional: true, nullable: false, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| } as const; | } as const; | ||||||
|  |  | ||||||
|  | @ -309,6 +309,7 @@ export const paramDef = { | ||||||
| 	type: 'object', | 	type: 'object', | ||||||
| 	properties: { | 	properties: { | ||||||
| 		detail: { type: 'boolean', default: true }, | 		detail: { type: 'boolean', default: true }, | ||||||
|  | 		omitEmojiUrl: { type: 'boolean', default: false }, | ||||||
| 	}, | 	}, | ||||||
| 	required: [], | 	required: [], | ||||||
| } as const; | } as const; | ||||||
|  | @ -390,7 +391,7 @@ 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), | 				emojis: await this.emojiEntityService.packMany(emojis, { omitUrl: ps.omitEmojiUrl }), | ||||||
| 				defaultLightTheme: instance.defaultLightTheme, | 				defaultLightTheme: instance.defaultLightTheme, | ||||||
| 				defaultDarkTheme: instance.defaultDarkTheme, | 				defaultDarkTheme: instance.defaultDarkTheme, | ||||||
| 				ads: ads.map(ad => ({ | 				ads: ads.map(ad => ({ | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js'; | ||||||
| import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; | import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; | ||||||
| import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; | import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; | ||||||
| import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; | import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; | ||||||
| import type { ChannelsRepository, ClipsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; | import type { ChannelsRepository, ClipsRepository, EmojisRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; | ||||||
| import { deepClone } from '@/misc/clone.js'; | import { deepClone } from '@/misc/clone.js'; | ||||||
| import { bindThis } from '@/decorators.js'; | import { bindThis } from '@/decorators.js'; | ||||||
| import manifest from './manifest.json' assert { type: 'json' }; | import manifest from './manifest.json' assert { type: 'json' }; | ||||||
|  | @ -70,6 +70,9 @@ export class ClientServerService { | ||||||
| 		@Inject(DI.pagesRepository) | 		@Inject(DI.pagesRepository) | ||||||
| 		private pagesRepository: PagesRepository, | 		private pagesRepository: PagesRepository, | ||||||
| 
 | 
 | ||||||
|  | 		@Inject(DI.emojisRepository) | ||||||
|  | 		private emojisRepository: EmojisRepository, | ||||||
|  | 
 | ||||||
| 		private userEntityService: UserEntityService, | 		private userEntityService: UserEntityService, | ||||||
| 		private noteEntityService: NoteEntityService, | 		private noteEntityService: NoteEntityService, | ||||||
| 		private pageEntityService: PageEntityService, | 		private pageEntityService: PageEntityService, | ||||||
|  | @ -217,6 +220,33 @@ export class ClientServerService { | ||||||
| 			return reply.sendFile('/apple-touch-icon.png', staticAssets); | 			return reply.sendFile('/apple-touch-icon.png', staticAssets); | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
|  | 		fastify.get<{ Params: { path: string } }>('/emoji/:path(.*)', async (request, reply) => { | ||||||
|  | 			const path = request.params.path; | ||||||
|  | 
 | ||||||
|  | 			if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) { | ||||||
|  | 				reply.code(404); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			const name = path.split('@')[0].replace('.webp', ''); | ||||||
|  | 			const host = path.split('@')[1]?.replace('.webp', ''); | ||||||
|  | 
 | ||||||
|  | 			const emoji = await this.emojisRepository.findOneBy({ | ||||||
|  | 				host: host == null ? IsNull() : host, | ||||||
|  | 				name: name, | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			if (emoji == null) { | ||||||
|  | 				reply.code(404); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); | ||||||
|  | 
 | ||||||
|  | 			// ?? emoji.originalUrl してるのは後方互換性のため
 | ||||||
|  | 			return await reply.redirect(301, emoji.publicUrl ?? emoji.originalUrl); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
| 		fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => { | 		fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => { | ||||||
| 			const path = request.params.path; | 			const path = request.params.path; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -37,20 +37,20 @@ | ||||||
| 			<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/> | 			<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/> | ||||||
| 			<div class="body"> | 			<div class="body"> | ||||||
| 				<p v-if="appearNote.cw != null" class="cw"> | 				<p v-if="appearNote.cw != null" class="cw"> | ||||||
| 					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> | 					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i"/> | ||||||
| 					<XCwButton v-model="showContent" :note="appearNote"/> | 					<XCwButton v-model="showContent" :note="appearNote"/> | ||||||
| 				</p> | 				</p> | ||||||
| 				<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }"> | 				<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }"> | ||||||
| 					<div class="text"> | 					<div class="text"> | ||||||
| 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> | 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> | ||||||
| 						<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> | 						<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> | ||||||
| 						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> | 						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i"/> | ||||||
| 						<a v-if="appearNote.renote != null" class="rp">RN:</a> | 						<a v-if="appearNote.renote != null" class="rp">RN:</a> | ||||||
| 						<div v-if="translating || translation" class="translation"> | 						<div v-if="translating || translation" class="translation"> | ||||||
| 							<MkLoading v-if="translating" mini/> | 							<MkLoading v-if="translating" mini/> | ||||||
| 							<div v-else class="translated"> | 							<div v-else class="translated"> | ||||||
| 								<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b> | 								<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b> | ||||||
| 								<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> | 								<Mfm :text="translation.text" :author="appearNote.user" :i="$i"/> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
|  |  | ||||||
|  | @ -48,20 +48,20 @@ | ||||||
| 		<div class="main"> | 		<div class="main"> | ||||||
| 			<div class="body"> | 			<div class="body"> | ||||||
| 				<p v-if="appearNote.cw != null" class="cw"> | 				<p v-if="appearNote.cw != null" class="cw"> | ||||||
| 					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> | 					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i"/> | ||||||
| 					<XCwButton v-model="showContent" :note="appearNote"/> | 					<XCwButton v-model="showContent" :note="appearNote"/> | ||||||
| 				</p> | 				</p> | ||||||
| 				<div v-show="appearNote.cw == null || showContent" class="content"> | 				<div v-show="appearNote.cw == null || showContent" class="content"> | ||||||
| 					<div class="text"> | 					<div class="text"> | ||||||
| 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> | 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> | ||||||
| 						<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> | 						<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> | ||||||
| 						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> | 						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i"/> | ||||||
| 						<a v-if="appearNote.renote != null" class="rp">RN:</a> | 						<a v-if="appearNote.renote != null" class="rp">RN:</a> | ||||||
| 						<div v-if="translating || translation" class="translation"> | 						<div v-if="translating || translation" class="translation"> | ||||||
| 							<MkLoading v-if="translating" mini/> | 							<MkLoading v-if="translating" mini/> | ||||||
| 							<div v-else class="translated"> | 							<div v-else class="translated"> | ||||||
| 								<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b> | 								<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b> | ||||||
| 								<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> | 								<Mfm :text="translation.text" :author="appearNote.user" :i="$i"/> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| 		<XNoteHeader class="header" :note="note" :mini="true"/> | 		<XNoteHeader class="header" :note="note" :mini="true"/> | ||||||
| 		<div class="body"> | 		<div class="body"> | ||||||
| 			<p v-if="note.cw != null" class="cw"> | 			<p v-if="note.cw != null" class="cw"> | ||||||
| 				<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> | 				<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i"/> | ||||||
| 				<XCwButton v-model="showContent" :note="note"/> | 				<XCwButton v-model="showContent" :note="note"/> | ||||||
| 			</p> | 			</p> | ||||||
| 			<div v-show="note.cw == null || showContent" class="content"> | 			<div v-show="note.cw == null || showContent" class="content"> | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
| 			<XNoteHeader class="header" :note="note" :mini="true"/> | 			<XNoteHeader class="header" :note="note" :mini="true"/> | ||||||
| 			<div class="body"> | 			<div class="body"> | ||||||
| 				<p v-if="note.cw != null" class="cw"> | 				<p v-if="note.cw != null" class="cw"> | ||||||
| 					<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> | 					<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i"/> | ||||||
| 					<XCwButton v-model="showContent" :note="note"/> | 					<XCwButton v-model="showContent" :note="note"/> | ||||||
| 				</p> | 				</p> | ||||||
| 				<div v-show="note.cw == null || showContent" class="content"> | 				<div v-show="note.cw == null || showContent" class="content"> | ||||||
|  |  | ||||||
|  | @ -34,31 +34,31 @@ | ||||||
| 		</header> | 		</header> | ||||||
| 		<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | 		<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
| 			<i class="ti ti-quote"></i> | 			<i class="ti ti-quote"></i> | ||||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||||
| 			<i class="ti ti-quote"></i> | 			<i class="ti ti-quote"></i> | ||||||
| 		</MkA> | 		</MkA> | ||||||
| 		<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)"> | 		<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)"> | ||||||
| 			<i class="ti ti-quote"></i> | 			<i class="ti ti-quote"></i> | ||||||
| 			<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/> | ||||||
| 			<i class="ti ti-quote"></i> | 			<i class="ti ti-quote"></i> | ||||||
| 		</MkA> | 		</MkA> | ||||||
| 		<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | 		<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||||
| 		</MkA> | 		</MkA> | ||||||
| 		<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | 		<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||||
| 		</MkA> | 		</MkA> | ||||||
| 		<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | 		<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||||
| 		</MkA> | 		</MkA> | ||||||
| 		<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | 		<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
| 			<i class="ti ti-quote"></i> | 			<i class="ti ti-quote"></i> | ||||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||||
| 			<i class="ti ti-quote"></i> | 			<i class="ti ti-quote"></i> | ||||||
| 		</MkA> | 		</MkA> | ||||||
| 		<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | 		<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
| 			<i class="ti ti-quote"></i> | 			<i class="ti ti-quote"></i> | ||||||
| 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> | ||||||
| 			<i class="ti ti-quote"></i> | 			<i class="ti ti-quote"></i> | ||||||
| 		</MkA> | 		</MkA> | ||||||
| 		<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> | 		<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| 			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> | 			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> | ||||||
| 			<span> | 			<span> | ||||||
| 				<template v-if="choice.isVoted"><i class="ti ti-check"></i></template> | 				<template v-if="choice.isVoted"><i class="ti ti-check"></i></template> | ||||||
| 				<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/> | 				<Mfm :text="choice.text" :plain="true"/> | ||||||
| 				<span v-if="showResult" class="votes">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span> | 				<span v-if="showResult" class="votes">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span> | ||||||
| 			</span> | 			</span> | ||||||
| 		</li> | 		</li> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <template> | ||||||
| <MkEmoji :emoji="reaction" :custom-emojis="customEmojis || []" :is-reaction="true" :normal="true" :no-style="noStyle"/> | <MkEmoji :emoji="reaction" :is-reaction="true" :normal="true" :no-style="noStyle"/> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -7,7 +7,6 @@ import { } from 'vue'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	reaction: string; | 	reaction: string; | ||||||
| 	customEmojis?: any[]; // TODO |  | ||||||
| 	noStyle?: boolean; | 	noStyle?: boolean; | ||||||
| }>(); | }>(); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> | <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> | ||||||
| 	<div class="beeadbfb"> | 	<div class="beeadbfb"> | ||||||
| 		<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> | 		<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/> | ||||||
| 		<div class="name">{{ reaction.replace('@.', '') }}</div> | 		<div class="name">{{ reaction.replace('@.', '') }}</div> | ||||||
| 	</div> | 	</div> | ||||||
| </MkTooltip> | </MkTooltip> | ||||||
|  | @ -15,7 +15,6 @@ import XReactionIcon from '@/components/MkReactionIcon.vue'; | ||||||
| defineProps<{ | defineProps<{ | ||||||
| 	showing: boolean; | 	showing: boolean; | ||||||
| 	reaction: string; | 	reaction: string; | ||||||
| 	emojis: any[]; // TODO |  | ||||||
| 	targetElement: HTMLElement; | 	targetElement: HTMLElement; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> | <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> | ||||||
| 	<div class="bqxuuuey"> | 	<div class="bqxuuuey"> | ||||||
| 		<div class="reaction"> | 		<div class="reaction"> | ||||||
| 			<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> | 			<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/> | ||||||
| 			<div class="name">{{ getReactionName(reaction) }}</div> | 			<div class="name">{{ getReactionName(reaction) }}</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="users"> | 		<div class="users"> | ||||||
|  | @ -27,7 +27,6 @@ defineProps<{ | ||||||
| 	reaction: string; | 	reaction: string; | ||||||
| 	users: any[]; // TODO | 	users: any[]; // TODO | ||||||
| 	count: number; | 	count: number; | ||||||
| 	emojis: any[]; // TODO |  | ||||||
| 	targetElement: HTMLElement; | 	targetElement: HTMLElement; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ | ||||||
| 	:class="{ reacted: note.myReaction == reaction, canToggle }" | 	:class="{ reacted: note.myReaction == reaction, canToggle }" | ||||||
| 	@click="toggleReaction()" | 	@click="toggleReaction()" | ||||||
| > | > | ||||||
| 	<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/> | 	<XReactionIcon class="icon" :reaction="reaction"/> | ||||||
| 	<span class="count">{{ count }}</span> | 	<span class="count">{{ count }}</span> | ||||||
| </button> | </button> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 		<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> | 		<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> | ||||||
| 		<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> | 		<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> | ||||||
| 		<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> | 		<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> | ||||||
| 		<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> | 		<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/> | ||||||
| 		<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> | 		<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> | ||||||
| 	</div> | 	</div> | ||||||
| 	<details v-if="note.files.length > 0"> | 	<details v-if="note.files.length > 0"> | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ | ||||||
| 	<span v-if="$i && $i.id !== user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span> | 	<span v-if="$i && $i.id !== user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span> | ||||||
| 	<div class="description"> | 	<div class="description"> | ||||||
| 		<div v-if="user.description" class="mfm"> | 		<div v-if="user.description" class="mfm"> | ||||||
| 			<Mfm :text="user.description" :author="user" :i="$i" :custom-emojis="user.emojis"/> | 			<Mfm :text="user.description" :author="user" :i="$i"/> | ||||||
| 		</div> | 		</div> | ||||||
| 		<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span> | 		<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span> | ||||||
| 	</div> | 	</div> | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ | ||||||
| 				<p class="username"><MkAcct :user="user"/></p> | 				<p class="username"><MkAcct :user="user"/></p> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="description"> | 			<div class="description"> | ||||||
| 				<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i" :custom-emojis="user.emojis"/> | 				<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="status"> | 			<div class="status"> | ||||||
| 				<div> | 				<div> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <template> | ||||||
| <img v-if="customEmoji" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt" decoding="async"/> | <img v-if="isCustom" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt" decoding="async"/> | ||||||
| <img v-else-if="char && !useOsNativeEmojis" class="mk-emoji" :src="url" :alt="alt" decoding="async" @pointerenter="computeTitle"/> | <img v-else-if="char && !useOsNativeEmojis" class="mk-emoji" :src="url" :alt="alt" decoding="async" @pointerenter="computeTitle"/> | ||||||
| <span v-else-if="char && useOsNativeEmojis" :alt="alt" @pointerenter="computeTitle">{{ char }}</span> | <span v-else-if="char && useOsNativeEmojis" :alt="alt" @pointerenter="computeTitle">{{ char }}</span> | ||||||
| <span v-else>{{ emoji }}</span> | <span v-else>{{ emoji }}</span> | ||||||
|  | @ -7,44 +7,40 @@ | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| import { CustomEmoji } from 'misskey-js/built/entities'; |  | ||||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | ||||||
| import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; | import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
| import { instance } from '@/instance'; |  | ||||||
| import { getEmojiName } from '@/scripts/emojilist'; | import { getEmojiName } from '@/scripts/emojilist'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	emoji: string; | 	emoji: string; | ||||||
| 	normal?: boolean; | 	normal?: boolean; | ||||||
| 	noStyle?: boolean; | 	noStyle?: boolean; | ||||||
| 	customEmojis?: CustomEmoji[]; |  | ||||||
| 	isReaction?: boolean; | 	isReaction?: boolean; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; | const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; | ||||||
| 
 | 
 | ||||||
| const isCustom = computed(() => props.emoji.startsWith(':')); | const isCustom = computed(() => props.emoji.startsWith(':')); | ||||||
|  | const customEmojiName = props.emoji.substr(1, props.emoji.length - 2); | ||||||
| const char = computed(() => isCustom.value ? undefined : props.emoji); | const char = computed(() => isCustom.value ? undefined : props.emoji); | ||||||
| const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native' && !props.isReaction); | const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native' && !props.isReaction); | ||||||
| const ce = computed(() => props.customEmojis ?? instance.emojis ?? []); |  | ||||||
| const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : undefined); |  | ||||||
| const url = computed(() => { | const url = computed(() => { | ||||||
| 	if (char.value) { | 	if (char.value) { | ||||||
| 		return char2path(char.value); | 		return char2path(char.value); | ||||||
| 	} else { | 	} else { | ||||||
| 		const rawUrl = (customEmoji.value as CustomEmoji).url; | 		const rawUrl = '/emoji/' + customEmojiName + '.webp'; | ||||||
| 		return defaultStore.state.disableShowingAnimatedImages | 		return defaultStore.state.disableShowingAnimatedImages | ||||||
| 			? getStaticImageUrl(rawUrl) | 			? getStaticImageUrl(rawUrl) | ||||||
| 			: rawUrl; | 			: rawUrl; | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| const alt = computed(() => customEmoji.value ? `:${customEmoji.value.name}:` : char.value); | const alt = computed(() => isCustom.value ? `:${customEmojiName}:` : char.value); | ||||||
| 
 | 
 | ||||||
| // Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter | // Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter | ||||||
| function computeTitle(event: PointerEvent): void { | function computeTitle(event: PointerEvent): void { | ||||||
| 	const title = customEmoji.value | 	const title = isCustom.value | ||||||
| 		? `:${customEmoji.value.name}:` | 		? `:${customEmojiName}:` | ||||||
| 		: (getEmojiName(char.value as string) ?? char.value as string); | 		: (getEmojiName(char.value as string) ?? char.value as string); | ||||||
| 	(event.target as HTMLElement).title = title; | 	(event.target as HTMLElement).title = title; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <template> | ||||||
| <MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :customEmojis="customEmojis" :isNote="isNote" class="havbbuyv" :class="{ nowrap }"/> | <MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" class="havbbuyv" :class="{ nowrap }"/> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -11,7 +11,6 @@ const props = withDefaults(defineProps<{ | ||||||
| 	plain?: boolean; | 	plain?: boolean; | ||||||
| 	nowrap?: boolean; | 	nowrap?: boolean; | ||||||
| 	author?: any; | 	author?: any; | ||||||
| 	customEmojis?: any; |  | ||||||
| 	isNote?: boolean; | 	isNote?: boolean; | ||||||
| }>(), { | }>(), { | ||||||
| 	plain: false, | 	plain: false, | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <template> | ||||||
| <Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/> | <Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap"/> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  |  | ||||||
|  | @ -35,9 +35,6 @@ export default defineComponent({ | ||||||
| 			type: Object, | 			type: Object, | ||||||
| 			default: null, | 			default: null, | ||||||
| 		}, | 		}, | ||||||
| 		customEmojis: { |  | ||||||
| 			required: false, |  | ||||||
| 		}, |  | ||||||
| 		isNote: { | 		isNote: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			default: true, | 			default: true, | ||||||
|  | @ -275,7 +272,6 @@ export default defineComponent({ | ||||||
| 					return [h(MkEmoji, { | 					return [h(MkEmoji, { | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						emoji: `:${token.props.name}:`, | 						emoji: `:${token.props.name}:`, | ||||||
| 						customEmojis: this.customEmojis, |  | ||||||
| 						normal: this.plain, | 						normal: this.plain, | ||||||
| 					})]; | 					})]; | ||||||
| 				} | 				} | ||||||
|  | @ -284,7 +280,6 @@ export default defineComponent({ | ||||||
| 					return [h(MkEmoji, { | 					return [h(MkEmoji, { | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						emoji: token.props.emoji, | 						emoji: token.props.emoji, | ||||||
| 						customEmojis: this.customEmojis, |  | ||||||
| 						normal: this.plain, | 						normal: this.plain, | ||||||
| 					})]; | 					})]; | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ export const instance: Misskey.entities.InstanceMetadata = reactive(instanceData | ||||||
| export async function fetchInstance() { | export async function fetchInstance() { | ||||||
| 	const meta = await api('meta', { | 	const meta = await api('meta', { | ||||||
| 		detail: false, | 		detail: false, | ||||||
|  | 		omitEmojiUrl: true, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	for (const [k, v] of Object.entries(meta)) { | 	for (const [k, v] of Object.entries(meta)) { | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ | ||||||
| 					<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/> | 					<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/> | ||||||
| 					<div class="misskey">Misskey</div> | 					<div class="misskey">Misskey</div> | ||||||
| 					<div class="version">v{{ version }}</div> | 					<div class="version">v{{ version }}</div> | ||||||
| 					<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span> | 					<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :is-reaction="false" :normal="true" :no-style="true"/></span> | ||||||
| 				</div> | 				</div> | ||||||
| 				<div class="_formBlock" style="text-align: center;"> | 				<div class="_formBlock" style="text-align: center;"> | ||||||
| 					{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a> | 					{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a> | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ | ||||||
| 								<p class="acct">@{{ acct(req.follower) }}</p> | 								<p class="acct">@{{ acct(req.follower) }}</p> | ||||||
| 							</div> | 							</div> | ||||||
| 							<div v-if="req.follower.description" class="description" :title="req.follower.description"> | 							<div v-if="req.follower.description" class="description" :title="req.follower.description"> | ||||||
| 								<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/> | 								<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :plain="true" :nowrap="true"/> | ||||||
| 							</div> | 							</div> | ||||||
| 							<div class="actions"> | 							<div class="actions"> | ||||||
| 								<button class="_button" @click="accept(req.follower)"><i class="ti ti-check"></i></button> | 								<button class="_button" @click="accept(req.follower)"><i class="ti ti-check"></i></button> | ||||||
|  |  | ||||||
|  | @ -41,7 +41,7 @@ | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 					<div class="description"> | 					<div class="description"> | ||||||
| 						<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/> | 						<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i"/> | ||||||
| 						<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p> | 						<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p> | ||||||
| 					</div> | 					</div> | ||||||
| 					<div class="fields system"> | 					<div class="fields system"> | ||||||
|  | @ -61,10 +61,10 @@ | ||||||
| 					<div v-if="user.fields.length > 0" class="fields"> | 					<div v-if="user.fields.length > 0" class="fields"> | ||||||
| 						<dl v-for="(field, i) in user.fields" :key="i" class="field"> | 						<dl v-for="(field, i) in user.fields" :key="i" class="field"> | ||||||
| 							<dt class="name"> | 							<dt class="name"> | ||||||
| 								<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/> | 								<Mfm :text="field.name" :plain="true" :colored="false"/> | ||||||
| 							</dt> | 							</dt> | ||||||
| 							<dd class="value"> | 							<dd class="value"> | ||||||
| 								<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/> | 								<Mfm :text="field.value" :author="user" :i="$i" :colored="false"/> | ||||||
| 							</dd> | 							</dd> | ||||||
| 						</dl> | 						</dl> | ||||||
| 					</div> | 					</div> | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 		<div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb"> | 		<div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb"> | ||||||
| 			<div class="header"> | 			<div class="header"> | ||||||
| 				<MkAvatar class="avatar" :user="user"/> | 				<MkAvatar class="avatar" :user="user"/> | ||||||
| 				<MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/> | 				<MkReactionIcon class="reaction" :reaction="item.type" :no-style="true"/> | ||||||
| 				<MkTime :time="item.createdAt" class="createdAt"/> | 				<MkTime :time="item.createdAt" class="createdAt"/> | ||||||
| 			</div> | 			</div> | ||||||
| 			<MkNote :key="item.id" :note="item.note"/> | 			<MkNote :key="item.id" :note="item.note"/> | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| 			<div class="content _panel"> | 			<div class="content _panel"> | ||||||
| 				<div class="body"> | 				<div class="body"> | ||||||
| 					<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> | 					<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> | ||||||
| 					<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> | 					<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/> | ||||||
| 					<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> | 					<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> | ||||||
| 				</div> | 				</div> | ||||||
| 				<div v-if="note.files.length > 0" class="richcontent"> | 				<div v-if="note.files.length > 0" class="richcontent"> | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
| 				<span v-for="note in notes" :key="note.id" class="item"> | 				<span v-for="note in notes" :key="note.id" class="item"> | ||||||
| 					<img class="avatar" :src="note.user.avatarUrl" decoding="async"/> | 					<img class="avatar" :src="note.user.avatarUrl" decoding="async"/> | ||||||
| 					<MkA class="text" :to="notePage(note)"> | 					<MkA class="text" :to="notePage(note)"> | ||||||
| 						<Mfm class="text" :text="getNoteSummary(note)" :plain="true" :nowrap="true" :custom-emojis="note.emojis"/> | 						<Mfm class="text" :text="getNoteSummary(note)" :plain="true" :nowrap="true"/> | ||||||
| 					</MkA> | 					</MkA> | ||||||
| 					<span class="divider"></span> | 					<span class="divider"></span> | ||||||
| 				</span> | 				</span> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue