Improve emoji-picker (#5515)
* Improve emoji-picker * remove unimplanted translation * カテゴリのサジェスト * use unique
This commit is contained in:
parent
97b6af62fe
commit
4c6c06c80a
11 changed files with 169 additions and 32 deletions
|
@ -673,7 +673,9 @@ common/views/components/reaction-picker.vue:
|
|||
input-reaction-placeholder: "または絵文字を入力"
|
||||
|
||||
common/views/components/emoji-picker.vue:
|
||||
recent-emoji: "最近使った絵文字"
|
||||
custom-emoji: "カスタム絵文字"
|
||||
no-category: "カテゴリなし"
|
||||
people: "人"
|
||||
animals-and-nature: "動物&自然"
|
||||
food-and-drink: "食べ物&飲み物"
|
||||
|
@ -1591,6 +1593,7 @@ admin/views/emoji.vue:
|
|||
title: "絵文字の登録"
|
||||
name: "絵文字名"
|
||||
name-desc: "a~z 0~9 _ の文字が使えます。"
|
||||
category: "カテゴリ"
|
||||
aliases: "エイリアス"
|
||||
aliases-desc: "スペースで区切って複数設定できます。"
|
||||
url: "絵文字画像URL"
|
||||
|
|
13
migration/1571220798684-CustomEmojiCategory.ts
Normal file
13
migration/1571220798684-CustomEmojiCategory.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class CustomEmojiCategory1571220798684 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`ALTER TABLE "emoji" ADD "category" character varying(128)`, undefined);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "category"`, undefined);
|
||||
}
|
||||
|
||||
}
|
|
@ -8,6 +8,9 @@
|
|||
<span>{{ $t('add-emoji.name') }}</span>
|
||||
<template #desc>{{ $t('add-emoji.name-desc') }}</template>
|
||||
</ui-input>
|
||||
<ui-input v-model="category" :datalist="categoryList">
|
||||
<span>{{ $t('add-emoji.category') }}</span>
|
||||
</ui-input>
|
||||
<ui-input v-model="aliases">
|
||||
<span>{{ $t('add-emoji.aliases') }}</span>
|
||||
<template #desc>{{ $t('add-emoji.aliases-desc') }}</template>
|
||||
|
@ -24,7 +27,7 @@
|
|||
|
||||
<ui-card>
|
||||
<template #title><fa :icon="faGrin"/> {{ $t('emojis.title') }}</template>
|
||||
<section v-for="emoji in emojis" class="oryfrbft">
|
||||
<section v-for="emoji in emojis" :key="emoji.name" class="oryfrbft">
|
||||
<div>
|
||||
<img :src="emoji.url" :alt="emoji.name" style="width: 64px;"/>
|
||||
</div>
|
||||
|
@ -33,6 +36,9 @@
|
|||
<ui-input v-model="emoji.name">
|
||||
<span>{{ $t('add-emoji.name') }}</span>
|
||||
</ui-input>
|
||||
<ui-input v-model="emoji.category" :datalist="categoryList">
|
||||
<span>{{ $t('add-emoji.category') }}</span>
|
||||
</ui-input>
|
||||
<ui-input v-model="emoji.aliases">
|
||||
<span>{{ $t('add-emoji.aliases') }}</span>
|
||||
</ui-input>
|
||||
|
@ -55,12 +61,14 @@
|
|||
import Vue from 'vue';
|
||||
import i18n from '../../i18n';
|
||||
import { faGrin } from '@fortawesome/free-regular-svg-icons';
|
||||
import { unique } from '../../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('admin/views/emoji.vue'),
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
category: '',
|
||||
url: '',
|
||||
aliases: '',
|
||||
emojis: [],
|
||||
|
@ -72,10 +80,17 @@ export default Vue.extend({
|
|||
this.fetchEmojis();
|
||||
},
|
||||
|
||||
computed: {
|
||||
categoryList() {
|
||||
return unique(this.emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.$root.api('admin/emoji/add', {
|
||||
name: this.name,
|
||||
category: this.category,
|
||||
url: this.url,
|
||||
aliases: this.aliases.split(' ').filter(x => x.length > 0)
|
||||
}).then(() => {
|
||||
|
@ -94,7 +109,6 @@ export default Vue.extend({
|
|||
|
||||
fetchEmojis() {
|
||||
this.$root.api('admin/emoji/list').then(emojis => {
|
||||
emojis.reverse();
|
||||
for (const e of emojis) {
|
||||
e.aliases = (e.aliases || []).join(' ');
|
||||
}
|
||||
|
@ -106,6 +120,7 @@ export default Vue.extend({
|
|||
this.$root.api('admin/emoji/update', {
|
||||
id: emoji.id,
|
||||
name: emoji.name,
|
||||
category: emoji.category,
|
||||
url: emoji.url,
|
||||
aliases: emoji.aliases.split(' ').filter(x => x.length > 0)
|
||||
}).then(() => {
|
||||
|
|
|
@ -11,25 +11,46 @@
|
|||
</button>
|
||||
</header>
|
||||
<div class="emojis">
|
||||
<header><fa :icon="categories.find(x => x.isActive).icon" fixed-width/> {{ categories.find(x => x.isActive).text }}</header>
|
||||
<div v-if="categories.find(x => x.isActive).name">
|
||||
<button v-for="emoji in emojilist.filter(e => e.category === categories.find(x => x.isActive).name)"
|
||||
:title="emoji.name"
|
||||
@click="chosen(emoji.char)"
|
||||
:key="emoji.name"
|
||||
>
|
||||
<mk-emoji :emoji="emoji.char"/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button v-for="emoji in customEmojis"
|
||||
:title="emoji.name"
|
||||
@click="chosen(`:${emoji.name}:`)"
|
||||
:key="emoji.name"
|
||||
>
|
||||
<img :src="emoji.url" :alt="emoji.name"/>
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="categories[0].isActive">
|
||||
<header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recent-emoji') }}</header>
|
||||
<div class="list">
|
||||
<button v-for="(emoji, i) in ($store.state.device.recentEmojis || [])"
|
||||
:title="emoji.name"
|
||||
@click="chosen(emoji)"
|
||||
:key="i"
|
||||
>
|
||||
<mk-emoji v-if="emoji.char != null" :emoji="emoji.char"/>
|
||||
<img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<header class="category"><fa :icon="categories.find(x => x.isActive).icon" fixed-width/> {{ categories.find(x => x.isActive).text }}</header>
|
||||
<template v-if="categories.find(x => x.isActive).name">
|
||||
<div class="list">
|
||||
<button v-for="emoji in emojilist.filter(e => e.category === categories.find(x => x.isActive).name)"
|
||||
:title="emoji.name"
|
||||
@click="chosen(emoji)"
|
||||
:key="emoji.name"
|
||||
>
|
||||
<mk-emoji :emoji="emoji.char"/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="(key, i) in Object.keys(customEmojis)" :key="i">
|
||||
<header class="sub">{{ key || $t('no-category') }}</header>
|
||||
<div class="list">
|
||||
<button v-for="emoji in customEmojis[key]"
|
||||
:title="emoji.name"
|
||||
@click="chosen(emoji)"
|
||||
:key="emoji.name"
|
||||
>
|
||||
<img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -38,8 +59,10 @@
|
|||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import { emojilist } from '../../../../../misc/emojilist';
|
||||
import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice } from '@fortawesome/free-solid-svg-icons';
|
||||
import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url';
|
||||
import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faHeart, faFlag } from '@fortawesome/free-regular-svg-icons';
|
||||
import { groupByX } from '../../../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/emoji-picker.vue'),
|
||||
|
@ -47,7 +70,9 @@ export default Vue.extend({
|
|||
data() {
|
||||
return {
|
||||
emojilist,
|
||||
customEmojis: [],
|
||||
getStaticImageUrl,
|
||||
customEmojis: {},
|
||||
faGlobe, faHistory,
|
||||
categories: [{
|
||||
text: this.$t('custom-emoji'),
|
||||
icon: faAsterisk,
|
||||
|
@ -97,18 +122,43 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
created() {
|
||||
this.customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
|
||||
let local = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
|
||||
local = groupByX(local, (x: any) => x.category || '');
|
||||
this.customEmojis = local;
|
||||
|
||||
if (this.$store.state.device.activeEmojiCategoryName) {
|
||||
this.goCategory(this.$store.state.device.activeEmojiCategoryName);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
go(category) {
|
||||
go(category: any) {
|
||||
this.goCategory(category.name);
|
||||
},
|
||||
|
||||
goCategory(name: string) {
|
||||
let matched = false;
|
||||
for (const c of this.categories) {
|
||||
c.isActive = c.name === category.name;
|
||||
c.isActive = c.name === name;
|
||||
if (c.isActive) {
|
||||
matched = true;
|
||||
this.$store.commit('device/set', { key: 'activeEmojiCategoryName', value: c.name });
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
this.categories[0].isActive = true;
|
||||
}
|
||||
},
|
||||
|
||||
chosen(emoji) {
|
||||
this.$emit('chosen', emoji);
|
||||
chosen(emoji: any) {
|
||||
const getKey = (emoji: any) => emoji.char || `:${emoji.name}:`;
|
||||
|
||||
let recents = this.$store.state.device.recentEmojis || [];
|
||||
recents = recents.filter((e: any) => getKey(e) !== getKey(emoji));
|
||||
recents.unshift(emoji)
|
||||
this.$store.commit('device/set', { key: 'recentEmojis', value: recents.splice(0, 16) });
|
||||
|
||||
this.$emit('chosen', getKey(emoji));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -142,7 +192,7 @@ export default Vue.extend({
|
|||
overflow-y auto
|
||||
overflow-x hidden
|
||||
|
||||
> header
|
||||
> header.category
|
||||
position sticky
|
||||
top 0
|
||||
left 0
|
||||
|
@ -152,7 +202,12 @@ export default Vue.extend({
|
|||
color var(--text)
|
||||
font-size 12px
|
||||
|
||||
> div
|
||||
>>> header.sub
|
||||
padding 4px 8px
|
||||
color var(--text)
|
||||
font-size 12px
|
||||
|
||||
>>> div.list
|
||||
display grid
|
||||
grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr
|
||||
gap 4px
|
||||
|
@ -180,6 +235,7 @@ export default Vue.extend({
|
|||
left 0
|
||||
width 100%
|
||||
height 100%
|
||||
object-fit contain
|
||||
font-size 28px
|
||||
transition transform 0.2s ease
|
||||
pointer-events none
|
||||
|
|
|
@ -79,6 +79,8 @@ const defaultDeviceSettings = {
|
|||
enableMobileQuickNotificationView: false,
|
||||
roomGraphicsQuality: 'medium',
|
||||
roomUseOrthographicCamera: true,
|
||||
activeEmojiCategoryName: undefined,
|
||||
recentEmojis: [],
|
||||
};
|
||||
|
||||
export default (os: MiOS) => new Vuex.Store({
|
||||
|
|
|
@ -24,6 +24,11 @@ export class Emoji {
|
|||
})
|
||||
public host: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true
|
||||
})
|
||||
public category: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512,
|
||||
})
|
||||
|
|
|
@ -84,6 +84,19 @@ export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
|
|||
return groupBy((a, b) => f(a) === f(b), xs);
|
||||
}
|
||||
|
||||
export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
|
||||
return collections.reduce((obj: Record<string, T[]>, item: T) => {
|
||||
const key = keySelector(item);
|
||||
if (!obj.hasOwnProperty(key)) {
|
||||
obj[key] = [];
|
||||
}
|
||||
|
||||
obj[key].push(item);
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two arrays by lexicographical order
|
||||
*/
|
||||
|
|
|
@ -26,6 +26,10 @@ export const meta = {
|
|||
validator: $.str.min(1)
|
||||
},
|
||||
|
||||
category: {
|
||||
validator: $.optional.str
|
||||
},
|
||||
|
||||
aliases: {
|
||||
validator: $.optional.arr($.str.min(1)),
|
||||
default: [] as string[]
|
||||
|
@ -52,6 +56,7 @@ export default define(meta, async (ps, me) => {
|
|||
id: genId(),
|
||||
updatedAt: new Date(),
|
||||
name: ps.name,
|
||||
category: ps.category,
|
||||
host: null,
|
||||
aliases: ps.aliases,
|
||||
url: ps.url,
|
||||
|
|
|
@ -23,12 +23,19 @@ export const meta = {
|
|||
|
||||
export default define(meta, async (ps) => {
|
||||
const emojis = await Emojis.find({
|
||||
host: toPunyNullable(ps.host)
|
||||
where: {
|
||||
host: toPunyNullable(ps.host)
|
||||
},
|
||||
order: {
|
||||
category: 'ASC',
|
||||
name: 'ASC'
|
||||
}
|
||||
});
|
||||
|
||||
return emojis.map(e => ({
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
category: e.category,
|
||||
aliases: e.aliases,
|
||||
host: e.host,
|
||||
url: e.url
|
||||
|
|
|
@ -25,6 +25,10 @@ export const meta = {
|
|||
validator: $.str
|
||||
},
|
||||
|
||||
category: {
|
||||
validator: $.optional.str
|
||||
},
|
||||
|
||||
url: {
|
||||
validator: $.str
|
||||
},
|
||||
|
@ -53,6 +57,7 @@ export default define(meta, async (ps) => {
|
|||
await Emojis.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
name: ps.name,
|
||||
category: ps.category,
|
||||
aliases: ps.aliases,
|
||||
url: ps.url,
|
||||
type,
|
||||
|
|
|
@ -96,7 +96,19 @@ export const meta = {
|
|||
export default define(meta, async (ps, me) => {
|
||||
const instance = await fetchMeta(true);
|
||||
|
||||
const emojis = await Emojis.find({ where: { host: null }, cache: { id: 'meta_emojis', milliseconds: 3600000 } }); // 1 hour
|
||||
const emojis = await Emojis.find({
|
||||
where: {
|
||||
host: null
|
||||
},
|
||||
order: {
|
||||
category: 'ASC',
|
||||
name: 'ASC'
|
||||
},
|
||||
cache: {
|
||||
id: 'meta_emojis',
|
||||
milliseconds: 3600000 // 1 hour
|
||||
}
|
||||
});
|
||||
|
||||
const response: any = {
|
||||
maintainerName: instance.maintainerName,
|
||||
|
@ -144,6 +156,7 @@ export default define(meta, async (ps, me) => {
|
|||
id: e.id,
|
||||
aliases: e.aliases,
|
||||
name: e.name,
|
||||
category: e.category,
|
||||
url: e.url,
|
||||
})),
|
||||
enableEmail: instance.enableEmail,
|
||||
|
|
Loading…
Reference in a new issue