feat: Add the ability to search emojis (#5928)

This commit is contained in:
Sivin Varghese 2022-12-06 05:30:42 +05:30 committed by GitHub
parent c3b6e1a732
commit 87ef39ad9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 9529 additions and 74 deletions

View file

@ -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);
}
}
}

View file

@ -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 {

View file

@ -0,0 +1,6 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
}
}

View file

@ -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,

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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 {

View file

@ -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",