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;
|
||||
left: unset;
|
||||
position: absolute;
|
||||
|
||||
&::before {
|
||||
transform: rotate(0deg);
|
||||
left: var(--space-smaller);
|
||||
bottom: var(--space-minus-slab);
|
||||
}
|
||||
bottom: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,7 +132,6 @@ import { mapGetters } from 'vuex';
|
|||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
||||
import CannedResponse from './CannedResponse';
|
||||
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
||||
|
@ -163,6 +162,8 @@ import { trimContent, debounce } from '@chatwoot/utils';
|
|||
import wootConstants from 'dashboard/constants';
|
||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiInput,
|
||||
|
@ -401,7 +402,7 @@ export default {
|
|||
return conversationDisplayType !== CONDENSED;
|
||||
},
|
||||
emojiDialogClassOnExpanedLayout() {
|
||||
return this.isOnExpandedLayout && !this.popoutReplyBox
|
||||
return this.isOnExpandedLayout || this.popoutReplyBox
|
||||
? 'emoji-dialog--expanded'
|
||||
: '';
|
||||
},
|
||||
|
@ -984,13 +985,13 @@ export default {
|
|||
|
||||
.emoji-dialog {
|
||||
top: unset;
|
||||
bottom: 12px;
|
||||
bottom: var(--space-normal);
|
||||
left: -320px;
|
||||
right: unset;
|
||||
|
||||
&::before {
|
||||
right: -16px;
|
||||
bottom: 10px;
|
||||
right: var(--space-minus-normal);
|
||||
bottom: var(--space-small);
|
||||
transform: rotate(270deg);
|
||||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
|
@ -1004,7 +1005,7 @@ export default {
|
|||
&::before {
|
||||
transform: rotate(0deg);
|
||||
left: var(--space-smaller);
|
||||
bottom: var(--space-minus-slab);
|
||||
bottom: var(--space-minus-small);
|
||||
}
|
||||
}
|
||||
.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 contactFilters from './contactFilters.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 helpCenter from './helpCenter.json';
|
||||
import inboxMgmt from './inboxMgmt.json';
|
||||
|
@ -40,7 +41,8 @@ export default {
|
|||
...contact,
|
||||
...contactFilters,
|
||||
...conversation,
|
||||
...csatMgmtMgmt,
|
||||
...csatMgmt,
|
||||
...emoji,
|
||||
...generalSettings,
|
||||
...helpCenter,
|
||||
...inboxMgmt,
|
||||
|
|
|
@ -1,41 +1,92 @@
|
|||
<template>
|
||||
<div role="dialog" class="emoji-dialog">
|
||||
<header class="emoji-dialog--header" role="menu">
|
||||
<ul>
|
||||
<li
|
||||
v-for="category in Object.keys(emojis)"
|
||||
:key="category"
|
||||
@click="changeCategory(category)"
|
||||
>
|
||||
<div class="emoji-list--wrap">
|
||||
<div class="emoji-search--wrap">
|
||||
<input
|
||||
ref="searchbar"
|
||||
v-model="search"
|
||||
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
|
||||
v-dompurify-html="emojis[category][0]"
|
||||
v-for="item in filterEmojisByCategory"
|
||||
:key="item.slug"
|
||||
v-dompurify-html="item.emoji"
|
||||
class="emoji--item"
|
||||
:class="{ active: selectedKey === category }"
|
||||
@click="changeCategory(category)"
|
||||
track-by="$index"
|
||||
@click="onClick(item.emoji)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
<h5 class="emoji-category--title">
|
||||
{{ selectedKey }}
|
||||
</h5>
|
||||
<div class="emoji--row">
|
||||
<button
|
||||
v-for="emoji in emojis[selectedKey]"
|
||||
:key="emoji"
|
||||
v-dompurify-html="emoji"
|
||||
class="emoji--item"
|
||||
track-by="$index"
|
||||
@click="onClick(emoji)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else ref="emojiItem" class="emoji-item">
|
||||
<div v-for="category in filterAllEmojisBySearch" :key="category.slug">
|
||||
<h5 v-if="category.emojis.length > 0" class="emoji-category--title">
|
||||
{{ category.name }}
|
||||
</h5>
|
||||
<div v-if="category.emojis.length > 0" class="emoji--row">
|
||||
<button
|
||||
v-for="item in category.emojis"
|
||||
:key="item.slug"
|
||||
v-dompurify-html="item.emoji"
|
||||
class="emoji--item"
|
||||
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>
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
components: { FluentIcon },
|
||||
props: {
|
||||
onClick: {
|
||||
type: Function,
|
||||
|
@ -44,18 +95,70 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
selectedKey: 'Smileys & Emotion',
|
||||
selectedKey: 'Search',
|
||||
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: {
|
||||
changeCategory(category) {
|
||||
this.search = '';
|
||||
this.$refs.emojiItem.scrollTo({ top: 0 });
|
||||
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>
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss">
|
||||
/**
|
||||
* 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-two: 20px;
|
||||
$space-medium: 24px;
|
||||
$space-large: 28px;
|
||||
$space-larger: 32px;
|
||||
|
||||
$font-size-tiny: 12px;
|
||||
$font-size-small: 14px;
|
||||
|
@ -76,52 +181,102 @@ $font-size-medium: 18px;
|
|||
|
||||
$color-bg: #ebf0f5;
|
||||
|
||||
$border-radius-normal: 5px;
|
||||
|
||||
.emoji-dialog {
|
||||
@include elegant-card;
|
||||
background: $color-white;
|
||||
border-radius: $space-small;
|
||||
box-sizing: content-box;
|
||||
height: 300px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -220px;
|
||||
width: 332px;
|
||||
top: -95px;
|
||||
width: 320px;
|
||||
z-index: 1;
|
||||
|
||||
&::before {
|
||||
@include arrow(bottom, $color-white, $space-slab);
|
||||
@include arrow(bottom, $color-bg, $space-slab);
|
||||
bottom: -$space-slab;
|
||||
position: absolute;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-size: $font-size-medium;
|
||||
height: $space-medium;
|
||||
border-radius: $space-smaller;
|
||||
height: 26px;
|
||||
line-height: 1.5;
|
||||
margin: $space-smaller;
|
||||
width: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 0 $space-smaller;
|
||||
padding: $space-small;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: $color-bg;
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1px $color-woot, 0 0 2px 3px $color-primary-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emoji--row {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: $space-smaller;
|
||||
flex-wrap: wrap;
|
||||
.empty-message {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 212px;
|
||||
justify-content: center;
|
||||
|
||||
.emoji--item {
|
||||
margin: $space-smaller;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.emoji-icon {
|
||||
color: var(--s-200);
|
||||
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 {
|
||||
|
@ -131,21 +286,20 @@ $color-bg: #ebf0f5;
|
|||
line-height: 1.5;
|
||||
margin: 0;
|
||||
padding: $space-smaller $space-small;
|
||||
margin-top: $space-smaller;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.emoji-dialog--header {
|
||||
.emoji-dialog--footer {
|
||||
background-color: $color-bg;
|
||||
border-top-left-radius: $space-small;
|
||||
border-top-right-radius: $space-small;
|
||||
bottom: 0;
|
||||
padding: 0 $space-smaller;
|
||||
position: sticky;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: $space-smaller 0;
|
||||
|
||||
> li {
|
||||
|
@ -160,6 +314,8 @@ $color-bg: #ebf0f5;
|
|||
background: $color-white;
|
||||
}
|
||||
.emoji--item {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: $font-size-small;
|
||||
|
||||
&: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 ChatSendButton from 'widget/components/ChatSendButton.vue';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||
|
||||
export default {
|
||||
name: 'ChatInputWrap',
|
||||
components: {
|
||||
|
@ -189,8 +190,8 @@ export default {
|
|||
}
|
||||
|
||||
.emoji-dialog {
|
||||
right: $space-smaller;
|
||||
top: -278px;
|
||||
right: 0;
|
||||
top: -302px;
|
||||
max-width: 100%;
|
||||
|
||||
&::before {
|
||||
|
|
|
@ -73,6 +73,10 @@
|
|||
"FIELD": "Invalid field"
|
||||
}
|
||||
},
|
||||
"EMOJI": {
|
||||
"PLACEHOLDER": "Search emojis",
|
||||
"NOT_FOUND": "No emoji match your search"
|
||||
},
|
||||
"CSAT": {
|
||||
"TITLE": "Rate your conversation",
|
||||
"SUBMITTED_TITLE": "Thank you for submitting the rating",
|
||||
|
|
Loading…
Reference in a new issue