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: "または絵文字を入力"
|
input-reaction-placeholder: "または絵文字を入力"
|
||||||
|
|
||||||
common/views/components/emoji-picker.vue:
|
common/views/components/emoji-picker.vue:
|
||||||
|
recent-emoji: "最近使った絵文字"
|
||||||
custom-emoji: "カスタム絵文字"
|
custom-emoji: "カスタム絵文字"
|
||||||
|
no-category: "カテゴリなし"
|
||||||
people: "人"
|
people: "人"
|
||||||
animals-and-nature: "動物&自然"
|
animals-and-nature: "動物&自然"
|
||||||
food-and-drink: "食べ物&飲み物"
|
food-and-drink: "食べ物&飲み物"
|
||||||
|
@ -1591,6 +1593,7 @@ admin/views/emoji.vue:
|
||||||
title: "絵文字の登録"
|
title: "絵文字の登録"
|
||||||
name: "絵文字名"
|
name: "絵文字名"
|
||||||
name-desc: "a~z 0~9 _ の文字が使えます。"
|
name-desc: "a~z 0~9 _ の文字が使えます。"
|
||||||
|
category: "カテゴリ"
|
||||||
aliases: "エイリアス"
|
aliases: "エイリアス"
|
||||||
aliases-desc: "スペースで区切って複数設定できます。"
|
aliases-desc: "スペースで区切って複数設定できます。"
|
||||||
url: "絵文字画像URL"
|
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>
|
<span>{{ $t('add-emoji.name') }}</span>
|
||||||
<template #desc>{{ $t('add-emoji.name-desc') }}</template>
|
<template #desc>{{ $t('add-emoji.name-desc') }}</template>
|
||||||
</ui-input>
|
</ui-input>
|
||||||
|
<ui-input v-model="category" :datalist="categoryList">
|
||||||
|
<span>{{ $t('add-emoji.category') }}</span>
|
||||||
|
</ui-input>
|
||||||
<ui-input v-model="aliases">
|
<ui-input v-model="aliases">
|
||||||
<span>{{ $t('add-emoji.aliases') }}</span>
|
<span>{{ $t('add-emoji.aliases') }}</span>
|
||||||
<template #desc>{{ $t('add-emoji.aliases-desc') }}</template>
|
<template #desc>{{ $t('add-emoji.aliases-desc') }}</template>
|
||||||
|
@ -24,7 +27,7 @@
|
||||||
|
|
||||||
<ui-card>
|
<ui-card>
|
||||||
<template #title><fa :icon="faGrin"/> {{ $t('emojis.title') }}</template>
|
<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>
|
<div>
|
||||||
<img :src="emoji.url" :alt="emoji.name" style="width: 64px;"/>
|
<img :src="emoji.url" :alt="emoji.name" style="width: 64px;"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,6 +36,9 @@
|
||||||
<ui-input v-model="emoji.name">
|
<ui-input v-model="emoji.name">
|
||||||
<span>{{ $t('add-emoji.name') }}</span>
|
<span>{{ $t('add-emoji.name') }}</span>
|
||||||
</ui-input>
|
</ui-input>
|
||||||
|
<ui-input v-model="emoji.category" :datalist="categoryList">
|
||||||
|
<span>{{ $t('add-emoji.category') }}</span>
|
||||||
|
</ui-input>
|
||||||
<ui-input v-model="emoji.aliases">
|
<ui-input v-model="emoji.aliases">
|
||||||
<span>{{ $t('add-emoji.aliases') }}</span>
|
<span>{{ $t('add-emoji.aliases') }}</span>
|
||||||
</ui-input>
|
</ui-input>
|
||||||
|
@ -55,12 +61,14 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
import { faGrin } from '@fortawesome/free-regular-svg-icons';
|
import { faGrin } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import { unique } from '../../../../prelude/array';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('admin/views/emoji.vue'),
|
i18n: i18n('admin/views/emoji.vue'),
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
|
category: '',
|
||||||
url: '',
|
url: '',
|
||||||
aliases: '',
|
aliases: '',
|
||||||
emojis: [],
|
emojis: [],
|
||||||
|
@ -72,10 +80,17 @@ export default Vue.extend({
|
||||||
this.fetchEmojis();
|
this.fetchEmojis();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
categoryList() {
|
||||||
|
return unique(this.emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
add() {
|
add() {
|
||||||
this.$root.api('admin/emoji/add', {
|
this.$root.api('admin/emoji/add', {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
category: this.category,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
aliases: this.aliases.split(' ').filter(x => x.length > 0)
|
aliases: this.aliases.split(' ').filter(x => x.length > 0)
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -94,7 +109,6 @@ export default Vue.extend({
|
||||||
|
|
||||||
fetchEmojis() {
|
fetchEmojis() {
|
||||||
this.$root.api('admin/emoji/list').then(emojis => {
|
this.$root.api('admin/emoji/list').then(emojis => {
|
||||||
emojis.reverse();
|
|
||||||
for (const e of emojis) {
|
for (const e of emojis) {
|
||||||
e.aliases = (e.aliases || []).join(' ');
|
e.aliases = (e.aliases || []).join(' ');
|
||||||
}
|
}
|
||||||
|
@ -106,6 +120,7 @@ export default Vue.extend({
|
||||||
this.$root.api('admin/emoji/update', {
|
this.$root.api('admin/emoji/update', {
|
||||||
id: emoji.id,
|
id: emoji.id,
|
||||||
name: emoji.name,
|
name: emoji.name,
|
||||||
|
category: emoji.category,
|
||||||
url: emoji.url,
|
url: emoji.url,
|
||||||
aliases: emoji.aliases.split(' ').filter(x => x.length > 0)
|
aliases: emoji.aliases.split(' ').filter(x => x.length > 0)
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
|
|
@ -11,25 +11,46 @@
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="emojis">
|
<div class="emojis">
|
||||||
<header><fa :icon="categories.find(x => x.isActive).icon" fixed-width/> {{ categories.find(x => x.isActive).text }}</header>
|
<template v-if="categories[0].isActive">
|
||||||
<div v-if="categories.find(x => x.isActive).name">
|
<header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recent-emoji') }}</header>
|
||||||
<button v-for="emoji in emojilist.filter(e => e.category === categories.find(x => x.isActive).name)"
|
<div class="list">
|
||||||
:title="emoji.name"
|
<button v-for="(emoji, i) in ($store.state.device.recentEmojis || [])"
|
||||||
@click="chosen(emoji.char)"
|
:title="emoji.name"
|
||||||
:key="emoji.name"
|
@click="chosen(emoji)"
|
||||||
>
|
:key="i"
|
||||||
<mk-emoji :emoji="emoji.char"/>
|
>
|
||||||
</button>
|
<mk-emoji v-if="emoji.char != null" :emoji="emoji.char"/>
|
||||||
</div>
|
<img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
|
||||||
<div v-else>
|
</button>
|
||||||
<button v-for="emoji in customEmojis"
|
</div>
|
||||||
:title="emoji.name"
|
</template>
|
||||||
@click="chosen(`:${emoji.name}:`)"
|
|
||||||
:key="emoji.name"
|
<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">
|
||||||
<img :src="emoji.url" :alt="emoji.name"/>
|
<div class="list">
|
||||||
</button>
|
<button v-for="emoji in emojilist.filter(e => e.category === categories.find(x => x.isActive).name)"
|
||||||
</div>
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -38,8 +59,10 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import { emojilist } from '../../../../../misc/emojilist';
|
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 { faHeart, faFlag } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import { groupByX } from '../../../../../prelude/array';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('common/views/components/emoji-picker.vue'),
|
i18n: i18n('common/views/components/emoji-picker.vue'),
|
||||||
|
@ -47,7 +70,9 @@ export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
emojilist,
|
emojilist,
|
||||||
customEmojis: [],
|
getStaticImageUrl,
|
||||||
|
customEmojis: {},
|
||||||
|
faGlobe, faHistory,
|
||||||
categories: [{
|
categories: [{
|
||||||
text: this.$t('custom-emoji'),
|
text: this.$t('custom-emoji'),
|
||||||
icon: faAsterisk,
|
icon: faAsterisk,
|
||||||
|
@ -97,18 +122,43 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
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: {
|
methods: {
|
||||||
go(category) {
|
go(category: any) {
|
||||||
|
this.goCategory(category.name);
|
||||||
|
},
|
||||||
|
|
||||||
|
goCategory(name: string) {
|
||||||
|
let matched = false;
|
||||||
for (const c of this.categories) {
|
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) {
|
chosen(emoji: any) {
|
||||||
this.$emit('chosen', emoji);
|
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-y auto
|
||||||
overflow-x hidden
|
overflow-x hidden
|
||||||
|
|
||||||
> header
|
> header.category
|
||||||
position sticky
|
position sticky
|
||||||
top 0
|
top 0
|
||||||
left 0
|
left 0
|
||||||
|
@ -152,7 +202,12 @@ export default Vue.extend({
|
||||||
color var(--text)
|
color var(--text)
|
||||||
font-size 12px
|
font-size 12px
|
||||||
|
|
||||||
> div
|
>>> header.sub
|
||||||
|
padding 4px 8px
|
||||||
|
color var(--text)
|
||||||
|
font-size 12px
|
||||||
|
|
||||||
|
>>> div.list
|
||||||
display grid
|
display grid
|
||||||
grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr
|
grid-template-columns 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr
|
||||||
gap 4px
|
gap 4px
|
||||||
|
@ -180,6 +235,7 @@ export default Vue.extend({
|
||||||
left 0
|
left 0
|
||||||
width 100%
|
width 100%
|
||||||
height 100%
|
height 100%
|
||||||
|
object-fit contain
|
||||||
font-size 28px
|
font-size 28px
|
||||||
transition transform 0.2s ease
|
transition transform 0.2s ease
|
||||||
pointer-events none
|
pointer-events none
|
||||||
|
|
|
@ -79,6 +79,8 @@ const defaultDeviceSettings = {
|
||||||
enableMobileQuickNotificationView: false,
|
enableMobileQuickNotificationView: false,
|
||||||
roomGraphicsQuality: 'medium',
|
roomGraphicsQuality: 'medium',
|
||||||
roomUseOrthographicCamera: true,
|
roomUseOrthographicCamera: true,
|
||||||
|
activeEmojiCategoryName: undefined,
|
||||||
|
recentEmojis: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (os: MiOS) => new Vuex.Store({
|
export default (os: MiOS) => new Vuex.Store({
|
||||||
|
|
|
@ -24,6 +24,11 @@ export class Emoji {
|
||||||
})
|
})
|
||||||
public host: string | null;
|
public host: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128, nullable: true
|
||||||
|
})
|
||||||
|
public category: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 512,
|
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);
|
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
|
* Compare two arrays by lexicographical order
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -26,6 +26,10 @@ export const meta = {
|
||||||
validator: $.str.min(1)
|
validator: $.str.min(1)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
category: {
|
||||||
|
validator: $.optional.str
|
||||||
|
},
|
||||||
|
|
||||||
aliases: {
|
aliases: {
|
||||||
validator: $.optional.arr($.str.min(1)),
|
validator: $.optional.arr($.str.min(1)),
|
||||||
default: [] as string[]
|
default: [] as string[]
|
||||||
|
@ -52,6 +56,7 @@ export default define(meta, async (ps, me) => {
|
||||||
id: genId(),
|
id: genId(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
|
category: ps.category,
|
||||||
host: null,
|
host: null,
|
||||||
aliases: ps.aliases,
|
aliases: ps.aliases,
|
||||||
url: ps.url,
|
url: ps.url,
|
||||||
|
|
|
@ -23,12 +23,19 @@ export const meta = {
|
||||||
|
|
||||||
export default define(meta, async (ps) => {
|
export default define(meta, async (ps) => {
|
||||||
const emojis = await Emojis.find({
|
const emojis = await Emojis.find({
|
||||||
host: toPunyNullable(ps.host)
|
where: {
|
||||||
|
host: toPunyNullable(ps.host)
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
category: 'ASC',
|
||||||
|
name: 'ASC'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return emojis.map(e => ({
|
return emojis.map(e => ({
|
||||||
id: e.id,
|
id: e.id,
|
||||||
name: e.name,
|
name: e.name,
|
||||||
|
category: e.category,
|
||||||
aliases: e.aliases,
|
aliases: e.aliases,
|
||||||
host: e.host,
|
host: e.host,
|
||||||
url: e.url
|
url: e.url
|
||||||
|
|
|
@ -25,6 +25,10 @@ export const meta = {
|
||||||
validator: $.str
|
validator: $.str
|
||||||
},
|
},
|
||||||
|
|
||||||
|
category: {
|
||||||
|
validator: $.optional.str
|
||||||
|
},
|
||||||
|
|
||||||
url: {
|
url: {
|
||||||
validator: $.str
|
validator: $.str
|
||||||
},
|
},
|
||||||
|
@ -53,6 +57,7 @@ export default define(meta, async (ps) => {
|
||||||
await Emojis.update(emoji.id, {
|
await Emojis.update(emoji.id, {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
|
category: ps.category,
|
||||||
aliases: ps.aliases,
|
aliases: ps.aliases,
|
||||||
url: ps.url,
|
url: ps.url,
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -96,7 +96,19 @@ export const meta = {
|
||||||
export default define(meta, async (ps, me) => {
|
export default define(meta, async (ps, me) => {
|
||||||
const instance = await fetchMeta(true);
|
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 = {
|
const response: any = {
|
||||||
maintainerName: instance.maintainerName,
|
maintainerName: instance.maintainerName,
|
||||||
|
@ -144,6 +156,7 @@ export default define(meta, async (ps, me) => {
|
||||||
id: e.id,
|
id: e.id,
|
||||||
aliases: e.aliases,
|
aliases: e.aliases,
|
||||||
name: e.name,
|
name: e.name,
|
||||||
|
category: e.category,
|
||||||
url: e.url,
|
url: e.url,
|
||||||
})),
|
})),
|
||||||
enableEmail: instance.enableEmail,
|
enableEmail: instance.enableEmail,
|
||||||
|
|
Loading…
Reference in a new issue