Skip to content

Commit

Permalink
リアクションピッカー絵文字ピッカーの改善 Resolve #2410 (#2445)
Browse files Browse the repository at this point in the history
* リアクションピッカー絵文字ピッカーの改善 Resolve #2410

* i.
  • Loading branch information
mei23 committed Mar 4, 2023
1 parent f661afa commit 4dee53e
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 166 deletions.
2 changes: 2 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,8 @@ common/views/components/emoji-picker.vue:
objects: "Objects"
symbols: "Symbols"
flags: "Flags"
search: Search

common/views/components/settings/app-type.vue:
title: "Mode"
intro: "You can specify whether you want to use the desktop, or the mobile layout."
Expand Down
1 change: 1 addition & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,7 @@ common/views/components/emoji-picker.vue:
objects: ""
symbols: "記号"
flags: ""
search: 検索

common/views/components/settings/app-type.vue:
title: "モード"
Expand Down
273 changes: 254 additions & 19 deletions src/client/app/common/views/components/emoji-picker.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<div class="prlncendiewqqkrevzeruhndoakghvtx">
<!-- タブのボタン -->
<header>
<button v-for="category in categories"
:title="category.text"
Expand All @@ -11,32 +12,62 @@
</button>
</header>
<div class="emojis">
<!-- ピン留め -->
<header v-if="!reaction" class="menu">
<ui-switch v-model="pinned">{{ $t('pinned') }}</ui-switch>
</header>
<!-- 検索 -->
<ui-input v-model="q" :autofocus="!$root.isMobile" style="margin: 0.8em 0.6em;">
<span>{{ $t('search') }}</span>
</ui-input>
<div class="list" v-if="searchResults.length > 0">
<button v-for="emoji in (searchResults || [])"
:title="emoji.sources ? emoji.sources.map(x => `${x.name}@${x.host}`).join(',\n') : emoji.name"
@click="chosen(emoji)"
:key="emoji.char || emoji.name"
>
<mk-emoji v-if="emoji.char != null" :emoji="emoji.char" :local="emoji.local"/>
<img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
</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"
<button v-for="emoji in ($store.state.device.recentEmojis || [])"
:title="emoji.sources ? emoji.sources.map(x => `${x.name}@${x.host}`).join(',\n') : emoji.name"
@click="chosen(emoji)"
:key="i"
:key="emoji.char || emoji.name"
>
<mk-emoji v-if="emoji.char != null" :emoji="emoji.char"/>
<mk-emoji v-if="emoji.char != null" :emoji="emoji.char" :local="emoji.local"/>
<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>
<header class="category">
<fa :icon="categories.find(x => x.isActive).icon" fixed-width/>
{{ categories.find(x => x.isActive).text }}
<div class="skinTones">
<button class="skinTone" v-for="st in SKIN_TONES" :key="st" @click="changeSkinTone(st)">
<mk-emoji :emoji="getSkinToneModifiedChar(SKIN_TONES_SAMPLE, st)"/>
</button>
</div>
</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"
@click="chosen(emoji, skinTone)"
:key="`${emoji.name}-${skinTone}`"
>
<mk-emoji :emoji="emoji.char"/>
<mk-emoji :emoji="emojiToSkinToneModifiedChar(emoji, skinTone)" :local="emoji.local"/>
</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>
Expand All @@ -56,22 +87,39 @@
</template>

<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import i18n from '../../../i18n';
import { emojilist } from '../../../../../misc/emojilist';
import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url';
import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory, faUser } from '@fortawesome/free-solid-svg-icons';
import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons';
import { faAsterisk, faUser, 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({
const SKIN_TONES_SAMPLE = '\u{1F44D}'; // thumbs up
const SKIN_TONES = [ null, '\u{1F3FB}', '\u{1F3FC}', '\u{1F3FD}', '\u{1F3FE}', '\u{1F3FF}' ];
export default defineComponent({
i18n: i18n('common/views/components/emoji-picker.vue'),
props: {
reaction: {
type: Boolean,
required: false,
default: false
},
},
data() {
return {
SKIN_TONES_SAMPLE,
SKIN_TONES,
pinned: false,
emojilist,
getStaticImageUrl,
customEmojis: {},
q: null,
searchResults: [],
skinTone: null,
faGlobe, faHistory,
categories: [{
text: this.$t('custom-emoji'),
Expand All @@ -80,7 +128,7 @@ export default Vue.extend({
}, {
name: 'face',
text: this.$t('face'),
icon: faLaugh,
icon: ['far', 'laugh'],
isActive: false
}, {
name: 'people',
Expand Down Expand Up @@ -126,6 +174,147 @@ export default Vue.extend({
}
},
watch: {
q() {
if (this.q == null || this.q === '') {
this.searchResults = [];
return;
}
const q = this.q.replace(/:/g, '');
const searchCustom = () => {
const max = 8;
const emojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
const matches = new Set();
const exactMatch = emojis.find(e => e.name === q);
if (exactMatch) matches.add(exactMatch);
if (q.includes(' ')) { // AND検索
const keywords = q.split(' ');
// 名前にキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
// 名前またはエイリアスにキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
} else {
for (const emoji of emojis) {
if (emoji.name.startsWith(q)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.startsWith(q))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.name.includes(q)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.includes(q))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
return matches;
};
const searchUnicode = () => {
const max = 8;
const emojis = this.emojilist;
const matches = new Set();
const exactMatch = emojis.find(e => e.name === q);
if (exactMatch) matches.add(exactMatch);
if (q.includes(' ')) { // AND検索
const keywords = q.split(' ');
// 名前にキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
// 名前またはエイリアスにキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
} else {
for (const emoji of emojis) {
if (emoji.name.startsWith(q)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.startsWith(q))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.name.includes(q)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.includes(q))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
return matches;
};
const searchResultCustom = Array.from(searchCustom());
const searchResultUnicode = Array.from(searchUnicode());
this.searchResults = searchResultCustom.concat(searchResultUnicode);
}
},
created() {
let local = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
local = groupByX(local, (x: any) => x.category || '');
Expand All @@ -134,6 +323,10 @@ export default Vue.extend({
if (this.$store.state.device.activeEmojiCategoryName) {
this.goCategory(this.$store.state.device.activeEmojiCategoryName);
}
if (SKIN_TONES.includes(this.$store.state.device.emojiSkinTone)) {
this.skinTone = this.$store.state.device.emojiSkinTone;
}
},
methods: {
Expand All @@ -155,15 +348,46 @@ export default Vue.extend({
}
},
chosen(emoji: any) {
const getKey = (emoji: any) => emoji.char || `:${emoji.name}:`;
changeSkinTone(skinTone: string) {
this.skinTone = skinTone;
this.$store.commit('device/set', { key: 'emojiSkinTone', value: skinTone });
},
emojiToSkinToneModifiedChar(emoji: any, skinTone: string | null | undefined): string {
if (emoji.st === 1) {
return this.getSkinToneModifiedChar(emoji.char, skinTone);
} else {
return emoji.char;
}
},
getSkinToneModifiedChar(char: string, skinTone: string | null | undefined): string {
if (!skinTone) return char;
let sgs = Array.from(char); // split by surrogate pair
// 2文字目に挿入するが、そこが絵文字セレクタなら置き換える
if (sgs[1] === '\u{FE0F}') {
sgs.splice(1, 1, skinTone);
} else {
sgs.splice(1, 0, skinTone);
}
return sgs.join('');
},
chosen(emoji: any, skinTone?: string) {
const getKey = (emoji: any) => emoji.char ? emoji.st === 1 ? this.getSkinToneModifiedChar(emoji.char, skinTone) : 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));
this.$emit('chosen', {
emoji: getKey(emoji),
close: !this.pinned,
});
}
}
});
Expand All @@ -172,7 +396,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
.prlncendiewqqkrevzeruhndoakghvtx
width 350px
background var(--face)
background var(--secondary)
> header
display flex
Expand All @@ -193,20 +417,31 @@ export default Vue.extend({
transition color 0s
> .emojis
height 300px
height 350px
overflow-y auto
overflow-x hidden
> header.menu
padding 0.5em
> header.category
position sticky
top 0
left 0
z-index 1
padding 8px
background var(--faceHeader)
background var(--popupBg)
color var(--text)
font-size 12px
> .skinTones
display inline-flex
position absolute
right 8px
> .skinTone
padding: 0 3px
>>> header.sub
padding 4px 8px
color var(--text)
Expand Down
Loading

0 comments on commit 4dee53e

Please sign in to comment.