feat: Add the ability to search emojis (#5928)
This commit is contained in:
parent
c3b6e1a732
commit
87ef39ad9c
9 changed files with 9529 additions and 74 deletions
|
@ -433,12 +433,7 @@ export default {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: unset;
|
left: unset;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
bottom: var(--space-smaller);
|
||||||
&::before {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
left: var(--space-smaller);
|
|
||||||
bottom: var(--space-minus-slab);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,7 +132,6 @@ import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
|
||||||
import CannedResponse from './CannedResponse';
|
import CannedResponse from './CannedResponse';
|
||||||
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
||||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
||||||
|
@ -163,6 +162,8 @@ import { trimContent, debounce } from '@chatwoot/utils';
|
||||||
import wootConstants from 'dashboard/constants';
|
import wootConstants from 'dashboard/constants';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
|
|
||||||
|
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EmojiInput,
|
EmojiInput,
|
||||||
|
@ -401,7 +402,7 @@ export default {
|
||||||
return conversationDisplayType !== CONDENSED;
|
return conversationDisplayType !== CONDENSED;
|
||||||
},
|
},
|
||||||
emojiDialogClassOnExpanedLayout() {
|
emojiDialogClassOnExpanedLayout() {
|
||||||
return this.isOnExpandedLayout && !this.popoutReplyBox
|
return this.isOnExpandedLayout || this.popoutReplyBox
|
||||||
? 'emoji-dialog--expanded'
|
? 'emoji-dialog--expanded'
|
||||||
: '';
|
: '';
|
||||||
},
|
},
|
||||||
|
@ -984,13 +985,13 @@ export default {
|
||||||
|
|
||||||
.emoji-dialog {
|
.emoji-dialog {
|
||||||
top: unset;
|
top: unset;
|
||||||
bottom: 12px;
|
bottom: var(--space-normal);
|
||||||
left: -320px;
|
left: -320px;
|
||||||
right: unset;
|
right: unset;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
right: -16px;
|
right: var(--space-minus-normal);
|
||||||
bottom: 10px;
|
bottom: var(--space-small);
|
||||||
transform: rotate(270deg);
|
transform: rotate(270deg);
|
||||||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
||||||
}
|
}
|
||||||
|
@ -1004,7 +1005,7 @@ export default {
|
||||||
&::before {
|
&::before {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
left: var(--space-smaller);
|
left: var(--space-smaller);
|
||||||
bottom: var(--space-minus-slab);
|
bottom: var(--space-minus-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.message-signature {
|
.message-signature {
|
||||||
|
|
6
app/javascript/dashboard/i18n/locale/en/emoji.json
Normal file
6
app/javascript/dashboard/i18n/locale/en/emoji.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"EMOJI": {
|
||||||
|
"PLACEHOLDER": "Search emojis",
|
||||||
|
"NOT_FOUND": "No emoji match your search"
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,8 @@ import chatlist from './chatlist.json';
|
||||||
import contact from './contact.json';
|
import contact from './contact.json';
|
||||||
import contactFilters from './contactFilters.json';
|
import contactFilters from './contactFilters.json';
|
||||||
import conversation from './conversation.json';
|
import conversation from './conversation.json';
|
||||||
import csatMgmtMgmt from './csatMgmt.json';
|
import csatMgmt from './csatMgmt.json';
|
||||||
|
import emoji from './emoji.json';
|
||||||
import generalSettings from './generalSettings.json';
|
import generalSettings from './generalSettings.json';
|
||||||
import helpCenter from './helpCenter.json';
|
import helpCenter from './helpCenter.json';
|
||||||
import inboxMgmt from './inboxMgmt.json';
|
import inboxMgmt from './inboxMgmt.json';
|
||||||
|
@ -40,7 +41,8 @@ export default {
|
||||||
...contact,
|
...contact,
|
||||||
...contactFilters,
|
...contactFilters,
|
||||||
...conversation,
|
...conversation,
|
||||||
...csatMgmtMgmt,
|
...csatMgmt,
|
||||||
|
...emoji,
|
||||||
...generalSettings,
|
...generalSettings,
|
||||||
...helpCenter,
|
...helpCenter,
|
||||||
...inboxMgmt,
|
...inboxMgmt,
|
||||||
|
|
|
@ -1,41 +1,92 @@
|
||||||
<template>
|
<template>
|
||||||
<div role="dialog" class="emoji-dialog">
|
<div role="dialog" class="emoji-dialog">
|
||||||
<header class="emoji-dialog--header" role="menu">
|
<div class="emoji-list--wrap">
|
||||||
<ul>
|
<div class="emoji-search--wrap">
|
||||||
<li
|
<input
|
||||||
v-for="category in Object.keys(emojis)"
|
ref="searchbar"
|
||||||
:key="category"
|
v-model="search"
|
||||||
@click="changeCategory(category)"
|
type="text"
|
||||||
>
|
class="emoji-search--input"
|
||||||
|
:placeholder="$t('EMOJI.PLACEHOLDER')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasNoSearch" ref="emojiItem" class="emoji-item">
|
||||||
|
<h5 class="emoji-category--title">
|
||||||
|
{{ selectedKey }}
|
||||||
|
</h5>
|
||||||
|
<div class="emoji--row">
|
||||||
<button
|
<button
|
||||||
v-dompurify-html="emojis[category][0]"
|
v-for="item in filterEmojisByCategory"
|
||||||
|
:key="item.slug"
|
||||||
|
v-dompurify-html="item.emoji"
|
||||||
class="emoji--item"
|
class="emoji--item"
|
||||||
:class="{ active: selectedKey === category }"
|
track-by="$index"
|
||||||
@click="changeCategory(category)"
|
@click="onClick(item.emoji)"
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
</header>
|
<div v-else ref="emojiItem" class="emoji-item">
|
||||||
<h5 class="emoji-category--title">
|
<div v-for="category in filterAllEmojisBySearch" :key="category.slug">
|
||||||
{{ selectedKey }}
|
<h5 v-if="category.emojis.length > 0" class="emoji-category--title">
|
||||||
</h5>
|
{{ category.name }}
|
||||||
<div class="emoji--row">
|
</h5>
|
||||||
<button
|
<div v-if="category.emojis.length > 0" class="emoji--row">
|
||||||
v-for="emoji in emojis[selectedKey]"
|
<button
|
||||||
:key="emoji"
|
v-for="item in category.emojis"
|
||||||
v-dompurify-html="emoji"
|
:key="item.slug"
|
||||||
class="emoji--item"
|
v-dompurify-html="item.emoji"
|
||||||
track-by="$index"
|
class="emoji--item"
|
||||||
@click="onClick(emoji)"
|
track-by="$index"
|
||||||
/>
|
@click="onClick(item.emoji)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasEmptySearchResult" class="empty-message">
|
||||||
|
<div class="emoji-icon">
|
||||||
|
<fluent-icon icon="emoji" size="48" />
|
||||||
|
</div>
|
||||||
|
<span class="empty-message--text">
|
||||||
|
{{ $t('EMOJI.NOT_FOUND') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="emoji-dialog--footer" role="menu">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="emoji--item"
|
||||||
|
:class="{ active: selectedKey === 'Search' }"
|
||||||
|
@click="changeCategory('Search')"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="search" size="16" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.slug"
|
||||||
|
@click="changeCategory(category.name)"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-dompurify-html="getFirstEmojiByCategoryName(category.name)"
|
||||||
|
class="emoji--item"
|
||||||
|
:class="{ active: selectedKey === category.name }"
|
||||||
|
@click="changeCategory(category.name)"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import emojis from './emojis.json';
|
import emojis from './emojisGroup.json';
|
||||||
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
|
const SEARCH_KEY = 'Search';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { FluentIcon },
|
||||||
props: {
|
props: {
|
||||||
onClick: {
|
onClick: {
|
||||||
type: Function,
|
type: Function,
|
||||||
|
@ -44,18 +95,70 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedKey: 'Smileys & Emotion',
|
selectedKey: 'Search',
|
||||||
emojis,
|
emojis,
|
||||||
|
search: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
categories() {
|
||||||
|
return [...this.emojis];
|
||||||
|
},
|
||||||
|
filterEmojisByCategory() {
|
||||||
|
const selectedCategoryName = this.emojis.find(category =>
|
||||||
|
category.name === this.selectedKey ? category.name : null
|
||||||
|
);
|
||||||
|
return selectedCategoryName?.emojis;
|
||||||
|
},
|
||||||
|
filterAllEmojisBySearch() {
|
||||||
|
return this.emojis.map(category => {
|
||||||
|
const allEmojis = category.emojis.filter(emoji =>
|
||||||
|
emoji.slug.replaceAll('_', ' ').includes(this.search.toLowerCase())
|
||||||
|
);
|
||||||
|
return allEmojis.length > 0
|
||||||
|
? { ...category, emojis: allEmojis }
|
||||||
|
: { ...category, emojis: [] };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hasNoSearch() {
|
||||||
|
return this.selectedKey !== SEARCH_KEY && this.search === '';
|
||||||
|
},
|
||||||
|
hasEmptySearchResult() {
|
||||||
|
return this.filterAllEmojisBySearch.every(
|
||||||
|
category => category.emojis.length === 0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
search() {
|
||||||
|
this.selectedKey = 'Search';
|
||||||
|
},
|
||||||
|
selectedKey() {
|
||||||
|
return this.selectedKey === 'Search' ? this.focusSearchInput() : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.focusSearchInput();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changeCategory(category) {
|
changeCategory(category) {
|
||||||
|
this.search = '';
|
||||||
|
this.$refs.emojiItem.scrollTo({ top: 0 });
|
||||||
this.selectedKey = category;
|
this.selectedKey = category;
|
||||||
},
|
},
|
||||||
|
getFirstEmojiByCategoryName(categoryName) {
|
||||||
|
const categoryItem = this.emojis.find(category =>
|
||||||
|
category.name === categoryName ? category : null
|
||||||
|
);
|
||||||
|
return categoryItem ? categoryItem.emojis[0].emoji : '';
|
||||||
|
},
|
||||||
|
focusSearchInput() {
|
||||||
|
this.$refs.searchbar.focus();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss">
|
||||||
/**
|
/**
|
||||||
* All the units used below are pixels due to variable name conflict in widget and dashboard
|
* All the units used below are pixels due to variable name conflict in widget and dashboard
|
||||||
**/
|
**/
|
||||||
|
@ -68,6 +171,8 @@ $space-slab: 12px;
|
||||||
$space-normal: 16px;
|
$space-normal: 16px;
|
||||||
$space-two: 20px;
|
$space-two: 20px;
|
||||||
$space-medium: 24px;
|
$space-medium: 24px;
|
||||||
|
$space-large: 28px;
|
||||||
|
$space-larger: 32px;
|
||||||
|
|
||||||
$font-size-tiny: 12px;
|
$font-size-tiny: 12px;
|
||||||
$font-size-small: 14px;
|
$font-size-small: 14px;
|
||||||
|
@ -76,52 +181,102 @@ $font-size-medium: 18px;
|
||||||
|
|
||||||
$color-bg: #ebf0f5;
|
$color-bg: #ebf0f5;
|
||||||
|
|
||||||
|
$border-radius-normal: 5px;
|
||||||
|
|
||||||
.emoji-dialog {
|
.emoji-dialog {
|
||||||
@include elegant-card;
|
@include elegant-card;
|
||||||
background: $color-white;
|
background: $color-white;
|
||||||
border-radius: $space-small;
|
border-radius: $space-small;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
height: 300px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: -220px;
|
top: -95px;
|
||||||
width: 332px;
|
width: 320px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@include arrow(bottom, $color-white, $space-slab);
|
@include arrow(bottom, $color-bg, $space-slab);
|
||||||
bottom: -$space-slab;
|
bottom: -$space-slab;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: $space-two;
|
right: $space-two;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-list--wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji--item {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: $space-smaller;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: $font-size-medium;
|
||||||
|
height: $space-medium;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 $space-smaller;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--s-75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji--row {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: $space-smaller;
|
||||||
|
|
||||||
.emoji--item {
|
.emoji--item {
|
||||||
cursor: pointer;
|
height: 26px;
|
||||||
background: transparent;
|
line-height: 1.5;
|
||||||
border: 0;
|
margin: $space-smaller;
|
||||||
font-size: $font-size-medium;
|
width: 26px;
|
||||||
height: $space-medium;
|
}
|
||||||
border-radius: $space-smaller;
|
}
|
||||||
|
|
||||||
|
.emoji-search--wrap {
|
||||||
|
margin: $space-small;
|
||||||
|
position: sticky;
|
||||||
|
top: $space-small;
|
||||||
|
|
||||||
|
.emoji-search--input {
|
||||||
|
background-color: $color-bg;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: $border-radius-normal;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
height: $space-larger;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 $space-smaller;
|
padding: $space-small;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&:hover {
|
&:focus {
|
||||||
background: $color-bg;
|
box-shadow: 0 0 0 1px $color-woot, 0 0 2px 3px $color-primary-light;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.emoji--row {
|
.empty-message {
|
||||||
display: flex;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
display: flex;
|
||||||
height: 200px;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
height: 212px;
|
||||||
padding: $space-smaller;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.emoji--item {
|
.emoji-icon {
|
||||||
margin: $space-smaller;
|
color: var(--s-200);
|
||||||
line-height: 1.5;
|
margin-bottom: $space-small;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.empty-message--text {
|
||||||
|
color: var(--s-200);
|
||||||
|
font-size: $font-size-small;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-item {
|
||||||
|
height: 212px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-category--title {
|
.emoji-category--title {
|
||||||
|
@ -131,21 +286,20 @@ $color-bg: #ebf0f5;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: $space-smaller $space-small;
|
padding: $space-smaller $space-small;
|
||||||
margin-top: $space-smaller;
|
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-dialog--header {
|
.emoji-dialog--footer {
|
||||||
background-color: $color-bg;
|
background-color: $color-bg;
|
||||||
border-top-left-radius: $space-small;
|
bottom: 0;
|
||||||
border-top-right-radius: $space-small;
|
|
||||||
padding: 0 $space-smaller;
|
padding: 0 $space-smaller;
|
||||||
|
position: sticky;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
overflow: auto;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
padding: $space-smaller 0;
|
padding: $space-smaller 0;
|
||||||
|
|
||||||
> li {
|
> li {
|
||||||
|
@ -160,6 +314,8 @@ $color-bg: #ebf0f5;
|
||||||
background: $color-white;
|
background: $color-white;
|
||||||
}
|
}
|
||||||
.emoji--item {
|
.emoji--item {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
File diff suppressed because one or more lines are too long
9291
app/javascript/shared/components/emoji/emojisGroup.json
Normal file
9291
app/javascript/shared/components/emoji/emojisGroup.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -53,11 +53,12 @@ import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import ChatAttachmentButton from 'widget/components/ChatAttachment.vue';
|
import ChatAttachmentButton from 'widget/components/ChatAttachment.vue';
|
||||||
import ChatSendButton from 'widget/components/ChatSendButton.vue';
|
import ChatSendButton from 'widget/components/ChatSendButton.vue';
|
||||||
import configMixin from '../mixins/configMixin';
|
import configMixin from '../mixins/configMixin';
|
||||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
|
||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
||||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
|
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChatInputWrap',
|
name: 'ChatInputWrap',
|
||||||
components: {
|
components: {
|
||||||
|
@ -189,8 +190,8 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-dialog {
|
.emoji-dialog {
|
||||||
right: $space-smaller;
|
right: 0;
|
||||||
top: -278px;
|
top: -302px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
|
|
@ -73,6 +73,10 @@
|
||||||
"FIELD": "Invalid field"
|
"FIELD": "Invalid field"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"EMOJI": {
|
||||||
|
"PLACEHOLDER": "Search emojis",
|
||||||
|
"NOT_FOUND": "No emoji match your search"
|
||||||
|
},
|
||||||
"CSAT": {
|
"CSAT": {
|
||||||
"TITLE": "Rate your conversation",
|
"TITLE": "Rate your conversation",
|
||||||
"SUBMITTED_TITLE": "Thank you for submitting the rating",
|
"SUBMITTED_TITLE": "Thank you for submitting the rating",
|
||||||
|
|
Loading…
Reference in a new issue