From f5f7654f4b9a99d3b57d55435e6d467084186c4d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 14 Feb 2020 01:09:39 +0900
Subject: [PATCH] Improve custom emoji managemant

---
 locales/ja-JP.yml                             |   3 +
 src/client/pages/instance/emojis.vue          | 100 +++++++++++-------
 src/client/scripts/select-file.ts             |   8 +-
 src/server/api/endpoints/admin/emoji/add.ts   |  47 +++-----
 .../api/endpoints/admin/emoji/update.ts       |   9 --
 5 files changed, 86 insertions(+), 81 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 6d9402650a..ae4f76f3d1 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -393,6 +393,9 @@ noGroups: "グループがありません"
 joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。"
 noHistory: "履歴はありません"
 disableAnimatedMfm: "動きのあるMFMを無効にする"
+doing: "やっています"
+category: "カテゴリ"
+tags: "タグ"
 
 _ago:
   unknown: "謎"
diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/instance/emojis.vue
index a53508db84..66b52492b9 100644
--- a/src/client/pages/instance/emojis.vue
+++ b/src/client/pages/instance/emojis.vue
@@ -2,10 +2,10 @@
 <div class="mk-instance-emojis">
 	<portal to="icon"><fa :icon="faLaugh"/></portal>
 	<portal to="title">{{ $t('customEmojis') }}</portal>
+
 	<section class="_card local">
 		<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojis') }}</div>
 		<div class="_content">
-			<input ref="file" type="file" style="display: none;" @change="onChangeFile"/>
 			<mk-pagination :pagination="pagination" class="emojis" ref="emojis">
 				<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
 				<template #default="{items}">
@@ -13,15 +13,25 @@
 						<img :src="emoji.url" class="img" :alt="emoji.name"/>
 						<div class="body">
 							<span class="name">{{ emoji.name }}</span>
+							<span class="info">
+								<b class="category">{{ emoji.category }}</b>
+								<span class="aliases">{{ emoji.aliases.join(' ') }}</span>
+							</span>
 						</div>
 					</div>
 				</template>
 			</mk-pagination>
 		</div>
-		<div class="_footer">
-			<mk-button inline primary @click="add()"><fa :icon="faPlus"/> {{ $t('addEmoji') }}</mk-button>
+		<div class="_content" v-if="selected">
+			<mk-input v-model="name"><span>{{ $t('name') }}</span></mk-input>
+			<mk-input v-model="category"><span>{{ $t('category') }}</span></mk-input>
+			<mk-input v-model="aliases"><span>{{ $t('tags') }}</span></mk-input>
+			<mk-button inline primary @click="update"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 			<mk-button inline :disabled="selected == null" @click="del()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</mk-button>
 		</div>
+		<div class="_footer">
+			<mk-button inline primary @click="add"><fa :icon="faPlus"/> {{ $t('addEmoji') }}</mk-button>
+		</div>
 	</section>
 	<section class="_card remote">
 		<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojisOfRemote') }}</div>
@@ -34,7 +44,7 @@
 						<img :src="emoji.url" class="img" :alt="emoji.name"/>
 						<div class="body">
 							<span class="name">{{ emoji.name }}</span>
-							<span class="host">{{ emoji.host }}</span>
+							<span class="info">{{ emoji.host }}</span>
 						</div>
 					</div>
 				</template>
@@ -49,12 +59,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { faPlus } from '@fortawesome/free-solid-svg-icons';
+import { faPlus, faSave } from '@fortawesome/free-solid-svg-icons';
 import { faTrashAlt, faLaugh } from '@fortawesome/free-regular-svg-icons';
 import MkButton from '../../components/ui/button.vue';
 import MkInput from '../../components/ui/input.vue';
 import MkPagination from '../../components/ui/pagination.vue';
-import { apiUrl } from '../../config';
+import { selectFile } from '../../scripts/select-file';
 
 export default Vue.extend({
 	metaInfo() {
@@ -71,9 +81,11 @@ export default Vue.extend({
 
 	data() {
 		return {
-			name: null,
 			selected: null,
 			selectedRemote: null,
+			name: null,
+			category: null,
+			aliases: null,
 			host: '',
 			pagination: {
 				endpoint: 'admin/emoji/list',
@@ -86,52 +98,38 @@ export default Vue.extend({
 					host: this.host ? this.host : null
 				})
 			},
-			faTrashAlt, faPlus, faLaugh
+			faTrashAlt, faPlus, faLaugh, faSave
 		}
 	},
 
 	watch: {
 		host() {
 			this.$refs.remoteEmojis.reload();
+		},
+
+		selected() {
+			this.name = this.selected ? this.selected.name : null;
+			this.category = this.selected ? this.selected.category : null;
+			this.aliases = this.selected ? this.selected.aliases.join(' ') : null;
 		}
 	},
 
 	methods: {
-		async add() {
-			const { canceled: canceled, result: name } = await this.$root.dialog({
-				title: this.$t('emojiName'),
-				input: true
-			});
-			if (canceled) return;
-
-			this.name = name;
-
-			(this.$refs.file as any).click();
-		},
-
-		onChangeFile() {
-			const [file] = Array.from((this.$refs.file as any).files);
-			if (file == null) return;
-			
-			const data = new FormData();
-			data.append('file', file);
-			data.append('name', this.name);
-			data.append('i', this.$store.state.i.token);
+		async add(e) {
+			const files = await selectFile(this, e.currentTarget || e.target, null, true);
 
 			const dialog = this.$root.dialog({
 				type: 'waiting',
-				text: this.$t('uploading') + '...',
+				text: this.$t('doing') + '...',
 				showOkButton: false,
 				showCancelButton: false,
 				cancelableByBgClick: false
 			});
-
-			fetch(apiUrl + '/admin/emoji/add', {
-				method: 'POST',
-				body: data
-			})
-			.then(response => response.json())
-			.then(f => {
+			
+			Promise.all(files.map(file => this.$root.api('admin/emoji/add', {
+				fileId: file.id,
+			})))
+			.then(() => {
 				this.$refs.emojis.reload();
 				this.$root.dialog({
 					type: 'success',
@@ -143,6 +141,22 @@ export default Vue.extend({
 			});
 		},
 
+		async update() {
+			await this.$root.api('admin/emoji/update', {
+				id: this.selected.id,
+				name: this.name,
+				category: this.category,
+				aliases: this.aliases.split(' '),
+			});
+
+			this.$root.dialog({
+				type: 'success',
+				iconOnly: true, autoClose: true
+			});
+
+			this.$refs.emojis.reload();
+		},
+
 		async del() {
 			const { canceled } = await this.$root.dialog({
 				type: 'warning',
@@ -207,6 +221,18 @@ export default Vue.extend({
 						> .name {
 							display: block;
 						}
+
+						> .info {
+							opacity: 0.5;
+
+							> .category {
+								margin-right: 16px;
+							}
+
+							> .aliases {
+								font-style: oblique;
+							}
+						}
 					}
 				}
 			}
@@ -241,7 +267,7 @@ export default Vue.extend({
 							display: block;
 						}
 
-						> .host {
+						> .info {
 							opacity: 0.5;
 						}
 					}
diff --git a/src/client/scripts/select-file.ts b/src/client/scripts/select-file.ts
index 1025b23e03..70e68e88c0 100644
--- a/src/client/scripts/select-file.ts
+++ b/src/client/scripts/select-file.ts
@@ -1,8 +1,8 @@
-import { faUpload, faCloud, faLink } from '@fortawesome/free-solid-svg-icons';
+import { faUpload, faCloud } from '@fortawesome/free-solid-svg-icons';
 import { selectDriveFile } from './select-drive-file';
 import { apiUrl } from '../config';
 
-export function selectFile(component: any, src: any, label: string, multiple = false) {
+export function selectFile(component: any, src: any, label: string | null, multiple = false) {
 	return new Promise((res, rej) => {
 		const chooseFileFromPc = () => {
 			const input = document.createElement('input');
@@ -56,10 +56,10 @@ export function selectFile(component: any, src: any, label: string, multiple = f
 		};
 
 		component.$root.menu({
-			items: [{
+			items: [label ? {
 				text: label,
 				type: 'label'
-			}, {
+			} : undefined, {
 				text: component.$t('upload'),
 				icon: faUpload,
 				action: chooseFileFromPc
diff --git a/src/server/api/endpoints/admin/emoji/add.ts b/src/server/api/endpoints/admin/emoji/add.ts
index 3a17760e53..610efbbe8f 100644
--- a/src/server/api/endpoints/admin/emoji/add.ts
+++ b/src/server/api/endpoints/admin/emoji/add.ts
@@ -1,11 +1,12 @@
 import $ from 'cafy';
 import define from '../../../define';
-import { detectUrlMime } from '../../../../../misc/detect-url-mime';
-import { Emojis } from '../../../../../models';
+import { Emojis, DriveFiles } from '../../../../../models';
 import { genId } from '../../../../../misc/gen-id';
 import { getConnection } from 'typeorm';
 import { insertModerationLog } from '../../../../../services/insert-moderation-log';
 import { ApiError } from '../../../error';
+import { ID } from '../../../../../misc/cafy-id';
+import rndstr from 'rndstr';
 
 export const meta = {
 	desc: {
@@ -18,52 +19,36 @@ export const meta = {
 	requireModerator: true,
 
 	params: {
-		name: {
-			validator: $.str.min(1)
+		fileId: {
+			validator: $.type(ID)
 		},
-
-		url: {
-			validator: $.str.min(1)
-		},
-
-		category: {
-			validator: $.optional.str
-		},
-
-		aliases: {
-			validator: $.optional.arr($.str.min(1)),
-			default: [] as string[]
-		}
 	},
 
 	errors: {
-		emojiAlredyExists: {
-			message: 'Emoji already exists.',
-			code: 'EMOJI_ALREADY_EXISTS',
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'MO_SUCH_FILE',
 			id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf'
 		}
 	}
 };
 
 export default define(meta, async (ps, me) => {
-	const type = await detectUrlMime(ps.url);
+	const file = await DriveFiles.findOne(ps.fileId);
 
-	const exists = await Emojis.findOne({
-		name: ps.name,
-		host: null
-	});
+	if (file == null) throw new ApiError(meta.errors.noSuchFile);
 
-	if (exists != null) throw new ApiError(meta.errors.emojiAlredyExists);
+	const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
 
 	const emoji = await Emojis.save({
 		id: genId(),
 		updatedAt: new Date(),
-		name: ps.name,
-		category: ps.category,
+		name: name,
+		category: null,
 		host: null,
-		aliases: ps.aliases,
-		url: ps.url,
-		type,
+		aliases: [],
+		url: file.url,
+		type: file.type,
 	});
 
 	await getConnection().queryResultCache!.remove(['meta_emojis']);
diff --git a/src/server/api/endpoints/admin/emoji/update.ts b/src/server/api/endpoints/admin/emoji/update.ts
index 0651b8d283..b6ecb39b43 100644
--- a/src/server/api/endpoints/admin/emoji/update.ts
+++ b/src/server/api/endpoints/admin/emoji/update.ts
@@ -1,6 +1,5 @@
 import $ from 'cafy';
 import define from '../../../define';
-import { detectUrlMime } from '../../../../../misc/detect-url-mime';
 import { ID } from '../../../../../misc/cafy-id';
 import { Emojis } from '../../../../../models';
 import { getConnection } from 'typeorm';
@@ -29,10 +28,6 @@ export const meta = {
 			validator: $.optional.str
 		},
 
-		url: {
-			validator: $.str
-		},
-
 		aliases: {
 			validator: $.arr($.str)
 		}
@@ -52,15 +47,11 @@ export default define(meta, async (ps) => {
 
 	if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
 
-	const type = await detectUrlMime(ps.url);
-
 	await Emojis.update(emoji.id, {
 		updatedAt: new Date(),
 		name: ps.name,
 		category: ps.category,
 		aliases: ps.aliases,
-		url: ps.url,
-		type,
 	});
 
 	await getConnection().queryResultCache!.remove(['meta_emojis']);