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

View file

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

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

View file

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

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

View file

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