summaryrefslogtreecommitdiff
path: root/src/client/components
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2020-11-07 10:43:27 +0900
committersyuilo <syuilotan@yahoo.co.jp>2020-11-07 10:43:27 +0900
commitbef2534fa86cc58654c23bbc8d59f9f9e756f762 (patch)
treedb8537d43d9bf7d88f8c88c7f33dab9d33ac18fc /src/client/components
parent画像ダイアログでスクロールが発生しないように (diff)
downloadsharkey-bef2534fa86cc58654c23bbc8d59f9f9e756f762.tar.gz
sharkey-bef2534fa86cc58654c23bbc8d59f9f9e756f762.tar.bz2
sharkey-bef2534fa86cc58654c23bbc8d59f9f9e756f762.zip
絵文字ピッカーを強化 + 絵文字ピッカーをリアクションピッカーとして使えるように
Resolve #5079 Resolve #3219
Diffstat (limited to 'src/client/components')
-rw-r--r--src/client/components/emoji-picker.vue424
-rw-r--r--src/client/components/note.vue44
2 files changed, 328 insertions, 140 deletions
diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue
index a9d7c71141..12f770205d 100644
--- a/src/client/components/emoji-picker.vue
+++ b/src/client/components/emoji-picker.vue
@@ -1,62 +1,94 @@
<template>
<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
<div class="omfetrab _popup">
- <header>
- <button v-for="(category, i) in categories"
- class="_button"
- @click="go(category)"
- :class="{ active: category.isActive }"
- :key="i"
- >
- <Fa :icon="category.icon" fixed-width/>
- </button>
- </header>
-
+ <input ref="search" class="search" v-model.trim="q" :placeholder="$t('search')" @paste.stop="paste" @keyup.enter="done()" autofocus>
<div class="emojis">
- <template v-if="categories[0].isActive">
- <header class="category"><Fa :icon="faHistory" fixed-width/> {{ $t('recentUsed') }}</header>
- <div class="list">
- <button v-for="emoji in ($store.state.device.recentEmojis || [])"
+ <section class="result">
+ <div v-if="searchResultCustom.length > 0">
+ <button v-for="emoji in searchResultCustom"
class="_button"
:title="emoji.name"
- @click="chosen(emoji)"
+ @click="chosen(emoji, $event)"
:key="emoji"
+ tabindex="0"
>
<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
<img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
</button>
</div>
-
- <header class="category"><Fa :icon="faAsterisk" fixed-width/> {{ $t('customEmojis') }}</header>
- </template>
-
- <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)"
+ <div v-if="searchResultUnicode.length > 0">
+ <button v-for="emoji in searchResultUnicode"
class="_button"
:title="emoji.name"
- @click="chosen(emoji)"
+ @click="chosen(emoji, $event)"
:key="emoji.name"
+ tabindex="0"
>
<MkEmoji :emoji="emoji.char"/>
</button>
</div>
- </template>
- <template v-else>
- <div v-for="(key, i) in Object.keys(customEmojis)" :key="i">
- <header class="sub" v-if="key">{{ key }}</header>
- <div class="list">
- <button v-for="emoji in customEmojis[key]"
+ </section>
+
+ <div class="index">
+ <section>
+ <div>
+ <button v-for="emoji in reactions || $store.state.settings.reactions"
+ class="_button"
+ :title="emoji.name"
+ @click="chosen(emoji, $event)"
+ :key="emoji"
+ tabindex="0"
+ >
+ <MkEmoji :emoji="emoji.startsWith(':') ? null : emoji" :name="emoji.startsWith(':') ? emoji.substr(1, emoji.length - 2) : null" :normal="true"/>
+ </button>
+ </div>
+ </section>
+
+ <section>
+ <header class="_acrylic"><Fa :icon="faHistory" fixed-width/> {{ $t('recentUsed') }}</header>
+ <div>
+ <button v-for="emoji in ($store.state.device.recentEmojis || [])"
class="_button"
:title="emoji.name"
- @click="chosen(emoji)"
- :key="emoji.name"
+ @click="chosen(emoji, $event)"
+ :key="emoji"
>
- <img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+ <MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
+ <img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
</button>
</div>
+ </section>
+
+ <div class="arrow"><Fa :icon="faChevronDown"/></div>
+ </div>
+
+ <section v-for="category in customEmojiCategories" :key="'custom:' + category" class="custom">
+ <header class="_acrylic" v-appear="() => visibleCategories[category] = true">{{ category || $t('other') }}</header>
+ <div v-if="visibleCategories[category]">
+ <button v-for="emoji in customEmojis.filter(e => e.category === category)"
+ class="_button"
+ :title="emoji.name"
+ @click="chosen(emoji, $event)"
+ :key="emoji.name"
+ >
+ <img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+ </button>
</div>
- </template>
+ </section>
+
+ <section v-for="category in categories" :key="category.name" class="unicode">
+ <header class="_acrylic" v-appear="() => category.isActive = true"><Fa :icon="category.icon" fixed-width/> {{ category.name }}</header>
+ <div v-if="category.isActive">
+ <button v-for="emoji in emojilist.filter(e => e.category === category.name)"
+ class="_button"
+ :title="emoji.name"
+ @click="chosen(emoji, $event)"
+ :key="emoji.name"
+ >
+ <MkEmoji :emoji="emoji.char"/>
+ </button>
+ </div>
+ </section>
</div>
</div>
</MkModal>
@@ -66,10 +98,11 @@
import { defineComponent, markRaw } from 'vue';
import { emojilist } from '../../misc/emojilist';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
-import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory, faUser } from '@fortawesome/free-solid-svg-icons';
+import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory, faUser, faChevronDown } from '@fortawesome/free-solid-svg-icons';
import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons';
-import { groupByX } from '../../prelude/array';
import MkModal from '@/components/ui/modal.vue';
+import Particle from '@/components/particle.vue';
+import * as os from '@/os';
export default defineComponent({
components: {
@@ -80,6 +113,9 @@ export default defineComponent({
src: {
required: false
},
+ reactions: {
+ required: false
+ },
},
emits: ['done', 'closed'],
@@ -88,12 +124,14 @@ export default defineComponent({
return {
emojilist: markRaw(emojilist),
getStaticImageUrl,
- customEmojis: {},
- faGlobe, faHistory,
+ customEmojiCategories: this.$store.getters['instance/emojiCategories'],
+ customEmojis: this.$store.state.instance.meta.emojis,
+ visibleCategories: {},
+ q: null,
+ searchResultCustom: [],
+ searchResultUnicode: [],
+ faGlobe, faHistory, faChevronDown,
categories: [{
- icon: faAsterisk,
- isActive: true
- }, {
name: 'face',
icon: faLaugh,
isActive: false
@@ -134,38 +172,149 @@ export default defineComponent({
};
},
- created() {
- let local = this.$store.state.instance.meta.emojis;
- local = groupByX(local, (x: any) => x.category || '');
- this.customEmojis = markRaw(local);
- },
+ watch: {
+ q() {
+ if (this.q == null || this.q === '') {
+ this.searchResultCustom = [];
+ this.searchResultUnicode = [];
+ return;
+ }
- methods: {
- go(category: any) {
- this.goCategory(category.name);
- },
+ const q = this.q.replace(/:/g, '');
+
+ const searchCustom = () => {
+ const max = 8;
+ const emojis = this.customEmojis;
+ const matches = new Set();
+
+ const exactMatch = emojis.find(e => e.name === q);
+ if (exactMatch) matches.add(exactMatch);
- goCategory(name: string) {
- let matched = false;
- for (const c of this.categories) {
- c.isActive = c.name === name;
- if (c.isActive) {
- matched = true;
+ 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);
+
+ 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;
+ };
+
+ this.searchResultCustom = Array.from(searchCustom());
+ this.searchResultUnicode = Array.from(searchUnicode());
+ }
+ },
+
+ mounted() {
+ this.$refs.search.focus();
+ },
+
+ methods: {
+ chosen(emoji: any, ev) {
+ if (ev) {
+ const el = ev.currentTarget || ev.target;
+ const rect = el.getBoundingClientRect();
+ const x = rect.left + (el.clientWidth / 2);
+ const y = rect.top + (el.clientHeight / 2);
+ os.popup(Particle, { x, y }, {}, 'end');
}
- if (!matched) {
- this.categories[0].isActive = true;
- }
- },
- chosen(emoji: any) {
- const getKey = (emoji: any) => emoji.char || `:${emoji.name}:`;
+ const getKey = (emoji: any) => typeof emoji === 'string' ? emoji : emoji.char || `:${emoji.name}:`;
+ this.$emit('done', getKey(emoji));
+ this.$refs.modal.close();
+
+ // 最近使った絵文字更新
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('done', getKey(emoji));
- this.$refs.modal.close();
+ },
+
+ paste(event) {
+ const paste = (event.clipboardData || window.clipboardData).getData('text');
+ if (this.done(paste)) {
+ event.preventDefault();
+ }
+ },
+
+ done(query) {
+ if (query == null) query = this.q;
+ if (query == null) return;
+ const q = query.replace(/:/g, '');
+ const exactMatchCustom = this.customEmojis.find(e => e.name === q);
+ if (exactMatchCustom) {
+ this.chosen(exactMatchCustom);
+ return true;
+ }
+ const exactMatchUnicode = this.emojilist.find(e => e.name === q);
+ if (exactMatchUnicode) {
+ this.chosen(exactMatchUnicode);
+ return true;
+ }
},
}
});
@@ -174,85 +323,108 @@ export default defineComponent({
<style lang="scss" scoped>
.omfetrab {
width: 350px;
+ contain: content;
- > header {
- display: flex;
-
- > button {
- flex: 1;
- padding: 10px 0;
- font-size: 16px;
- transition: color 0.2s ease;
-
- &:hover {
- color: var(--fgHighlighted);
- transition: color 0s;
- }
-
- &.active {
- color: var(--accent);
- transition: color 0s;
- }
- }
+ > .search {
+ width: 100%;
+ padding: 12px;
+ box-sizing: border-box;
+ font-size: 1em;
+ outline: none;
+ border: none;
+ background: transparent;
+ color: var(--fg);
}
> .emojis {
- height: 300px;
+ $height: 300px;
+
+ height: $height;
overflow-y: auto;
overflow-x: hidden;
- > header.category {
- position: sticky;
- top: 0;
- left: 0;
- z-index: 1;
- padding: 8px;
- background: var(--panel);
- font-size: 12px;
+ > .index {
+ min-height: $height;
+ position: relative;
+ border-bottom: solid 1px var(--divider);
+
+ > .arrow {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ padding: 16px 0;
+ text-align: center;
+ opacity: 0.5;
+ pointer-events: none;
+ }
}
- header.sub {
- padding: 4px 8px;
- font-size: 12px;
- }
+ section {
+ > header {
+ position: sticky;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ padding: 8px;
+ font-size: 12px;
+ }
- div.list {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
- gap: 4px;
- padding: 8px;
+ > div {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
+ gap: 4px;
+ padding: 8px;
- > button {
- position: relative;
- padding: 0;
- width: 100%;
+ > button {
+ position: relative;
+ padding: 0;
+ width: 100%;
- &:before {
- content: '';
- display: block;
- width: 1px;
- height: 0;
- padding-bottom: 100%;
- }
+ &:focus {
+ outline: solid 2px var(--focus);
+ z-index: 1;
+ }
+
+ &:before {
+ content: '';
+ display: block;
+ width: 1px;
+ height: 0;
+ padding-bottom: 100%;
+ }
+
+ &:hover {
+ > * {
+ transform: scale(1.2);
+ transition: transform 0s;
+ }
+ }
- &:hover {
> * {
- transform: scale(1.2);
- transition: transform 0s;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ font-size: 28px;
+ transition: transform 0.2s ease;
+ pointer-events: none;
}
}
+ }
- > * {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- object-fit: contain;
- font-size: 28px;
- transition: transform 0.2s ease;
- pointer-events: none;
- }
+ &.result {
+ border-bottom: solid 1px var(--divider);
+ }
+
+ &.unicode {
+ min-height: 384px;
+ }
+
+ &.custom {
+ min-height: 64px;
}
}
}
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 377496b402..53972d9f6f 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -498,20 +498,36 @@ export default defineComponent({
react(viaKeyboard = false) {
pleaseLogin();
this.blur();
- os.popup(import('@/components/reaction-picker.vue'), {
- showFocus: viaKeyboard,
- src: this.$refs.reactButton,
- }, {
- done: reaction => {
- if (reaction) {
- os.api('notes/reactions/create', {
- noteId: this.appearNote.id,
- reaction: reaction
- });
- }
- this.focus();
- },
- }, 'closed');
+ if (this.$store.state.device.useFullReactionPicker) {
+ os.popup(import('@/components/emoji-picker.vue'), {
+ src: this.$refs.reactButton,
+ }, {
+ done: reaction => {
+ if (reaction) {
+ os.api('notes/reactions/create', {
+ noteId: this.appearNote.id,
+ reaction: reaction
+ });
+ }
+ this.focus();
+ },
+ }, 'closed');
+ } else {
+ os.popup(import('@/components/reaction-picker.vue'), {
+ showFocus: viaKeyboard,
+ src: this.$refs.reactButton,
+ }, {
+ done: reaction => {
+ if (reaction) {
+ os.api('notes/reactions/create', {
+ noteId: this.appearNote.id,
+ reaction: reaction
+ });
+ }
+ this.focus();
+ },
+ }, 'closed');
+ }
},
reactDirectly(reaction) {