Commit 189bf0ed authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'ph/emojiPickerFrequentlyUsedEmojis' into 'master'

Added frequently used emojis to new picker

See merge request gitlab-org/gitlab!56332
parents 0ac30ddf 0361f42c
<script> <script>
import { GlIntersectionObserver } from '@gitlab/ui'; import { GlIntersectionObserver } from '@gitlab/ui';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { humanize } from '~/lib/utils/text_utility';
import EmojiGroup from './emoji_group.vue'; import EmojiGroup from './emoji_group.vue';
export default { export default {
...@@ -25,7 +25,7 @@ export default { ...@@ -25,7 +25,7 @@ export default {
}, },
computed: { computed: {
categoryTitle() { categoryTitle() {
return capitalizeFirstCharacter(this.category); return humanize(this.category);
}, },
}, },
methods: { methods: {
...@@ -33,9 +33,6 @@ export default { ...@@ -33,9 +33,6 @@ export default {
this.renderGroup = true; this.renderGroup = true;
this.$emit('appear', this.category); this.$emit('appear', this.category);
}, },
categoryDissappeared() {
this.renderGroup = false;
},
}, },
}; };
</script> </script>
......
<script> <script>
import { GlIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui'; import { GlIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
import { findLastIndex } from 'lodash';
import VirtualList from 'vue-virtual-scroll-list'; import VirtualList from 'vue-virtual-scroll-list';
import { CATEGORY_NAMES } from '~/emoji'; import { CATEGORY_NAMES } from '~/emoji';
import { CATEGORY_ICON_MAP } from '../constants'; import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
import Category from './category.vue'; import Category from './category.vue';
import EmojiList from './emoji_list.vue'; import EmojiList from './emoji_list.vue';
import { getEmojiCategories } from './utils'; import { addToFrequentlyUsed, getEmojiCategories, hasFrequentlyUsedEmojis } from './utils';
export default { export default {
components: { components: {
...@@ -25,13 +26,16 @@ export default { ...@@ -25,13 +26,16 @@ export default {
}, },
data() { data() {
return { return {
currentCategory: null, currentCategory: 0,
searchValue: '', searchValue: '',
}; };
}, },
computed: { computed: {
categoryNames() { categoryNames() {
return CATEGORY_NAMES.map((category) => ({ return CATEGORY_NAMES.filter((c) => {
if (c === FREQUENTLY_USED_KEY) return hasFrequentlyUsedEmojis();
return true;
}).map((category) => ({
name: category, name: category,
icon: CATEGORY_ICON_MAP[category], icon: CATEGORY_ICON_MAP[category],
})); }));
...@@ -50,6 +54,7 @@ export default { ...@@ -50,6 +54,7 @@ export default {
selectEmoji(name) { selectEmoji(name) {
this.$emit('click', name); this.$emit('click', name);
this.$refs.dropdown.hide(); this.$refs.dropdown.hide();
addToFrequentlyUsed(name);
}, },
getBoundaryElement() { getBoundaryElement() {
return document.querySelector('.content-wrapper') || 'scrollParent'; return document.querySelector('.content-wrapper') || 'scrollParent';
...@@ -58,6 +63,11 @@ export default { ...@@ -58,6 +63,11 @@ export default {
this.$refs.virtualScoller.setScrollTop(0); this.$refs.virtualScoller.setScrollTop(0);
this.$refs.virtualScoller.forceRender(); this.$refs.virtualScoller.forceRender();
}, },
async onScroll(event, { offset }) {
const categories = await getEmojiCategories();
this.currentCategory = findLastIndex(Object.values(categories), ({ top }) => offset >= top);
},
}, },
}; };
</script> </script>
...@@ -86,10 +96,10 @@ export default { ...@@ -86,10 +96,10 @@ export default {
class="gl-display-flex gl-mx-5 gl-border-b-solid gl-border-gray-100 gl-border-b-1" class="gl-display-flex gl-mx-5 gl-border-b-solid gl-border-gray-100 gl-border-b-1"
> >
<button <button
v-for="category in categoryNames" v-for="(category, index) in categoryNames"
:key="category.name" :key="category.name"
:class="{ :class="{
'gl-text-black-normal! emoji-picker-category-active': category.name === currentCategory, 'gl-text-black-normal! emoji-picker-category-active': index === currentCategory,
}" }"
type="button" type="button"
class="gl-border-0 gl-border-b-2 gl-border-b-solid gl-flex-fill-1 gl-text-gray-300 gl-pt-3 gl-pb-3 gl-bg-transparent emoji-picker-category-tab" class="gl-border-0 gl-border-b-2 gl-border-b-solid gl-flex-fill-1 gl-text-gray-300 gl-pt-3 gl-pb-3 gl-bg-transparent emoji-picker-category-tab"
...@@ -100,18 +110,20 @@ export default { ...@@ -100,18 +110,20 @@ export default {
</div> </div>
<emoji-list :search-value="searchValue"> <emoji-list :search-value="searchValue">
<template #default="{ filteredCategories }"> <template #default="{ filteredCategories }">
<virtual-list ref="virtualScoller" :size="258" :remain="1" :bench="2" variable> <virtual-list
ref="virtualScoller"
:size="258"
:remain="1"
:bench="2"
variable
:onscroll="onScroll"
>
<div <div
v-for="(category, categoryKey) in filteredCategories" v-for="(category, categoryKey) in filteredCategories"
:key="categoryKey" :key="categoryKey"
:style="{ height: category.height + 'px' }" :style="{ height: category.height + 'px' }"
> >
<category <category :category="categoryKey" :emojis="category.emojis" @click="selectEmoji" />
:category="categoryKey"
:emojis="category.emojis"
@appear="categoryAppeared"
@click="selectEmoji"
/>
</div> </div>
</virtual-list> </virtual-list>
</template> </template>
......
import { chunk, memoize } from 'lodash'; import Cookies from 'js-cookie';
import { chunk, memoize, uniq } from 'lodash';
import { initEmojiMap, getEmojiCategoryMap } from '~/emoji'; import { initEmojiMap, getEmojiCategoryMap } from '~/emoji';
import { EMOJIS_PER_ROW, EMOJI_ROW_HEIGHT, CATEGORY_ROW_HEIGHT } from '../constants'; import {
EMOJIS_PER_ROW,
EMOJI_ROW_HEIGHT,
CATEGORY_ROW_HEIGHT,
FREQUENTLY_USED_KEY,
FREQUENTLY_USED_COOKIE_KEY,
} from '../constants';
export const generateCategoryHeight = (emojisLength) => export const generateCategoryHeight = (emojisLength) =>
emojisLength * EMOJI_ROW_HEIGHT + CATEGORY_ROW_HEIGHT; emojisLength * EMOJI_ROW_HEIGHT + CATEGORY_ROW_HEIGHT;
export const getFrequentlyUsedEmojis = () => {
const savedEmojis = Cookies.get(FREQUENTLY_USED_COOKIE_KEY);
if (!savedEmojis) return null;
const emojis = chunk(uniq(savedEmojis.split(',')), 9);
return {
frequently_used: {
emojis,
top: 0,
height: generateCategoryHeight(emojis.length),
},
};
};
export const addToFrequentlyUsed = (emoji) => {
const frequentlyUsedEmojis = uniq(
(Cookies.get(FREQUENTLY_USED_COOKIE_KEY) || '')
.split(',')
.filter((e) => e)
.concat(emoji),
);
Cookies.set(FREQUENTLY_USED_COOKIE_KEY, frequentlyUsedEmojis.join(','), { expires: 365 });
};
export const hasFrequentlyUsedEmojis = () => getFrequentlyUsedEmojis() !== null;
export const getEmojiCategories = memoize(async () => { export const getEmojiCategories = memoize(async () => {
await initEmojiMap(); await initEmojiMap();
const categories = await getEmojiCategoryMap(); const categories = await getEmojiCategoryMap();
let top = 0; const frequentlyUsedEmojis = getFrequentlyUsedEmojis();
let top = frequentlyUsedEmojis
? frequentlyUsedEmojis.frequently_used.top + frequentlyUsedEmojis.frequently_used.height
: 0;
return Object.freeze( return Object.freeze(
Object.keys(categories).reduce((acc, category) => { Object.keys(categories)
.filter((c) => c !== FREQUENTLY_USED_KEY)
.reduce((acc, category) => {
const emojis = chunk(categories[category], EMOJIS_PER_ROW); const emojis = chunk(categories[category], EMOJIS_PER_ROW);
const height = generateCategoryHeight(emojis.length); const height = generateCategoryHeight(emojis.length);
const newAcc = { const newAcc = {
...@@ -22,6 +63,6 @@ export const getEmojiCategories = memoize(async () => { ...@@ -22,6 +63,6 @@ export const getEmojiCategories = memoize(async () => {
top += height; top += height;
return newAcc; return newAcc;
}, {}), }, frequentlyUsedEmojis || {}),
); );
}); });
export const FREQUENTLY_USED_KEY = 'frequently_used';
export const FREQUENTLY_USED_COOKIE_KEY = 'frequently_used_emojis';
export const CATEGORY_ICON_MAP = { export const CATEGORY_ICON_MAP = {
[FREQUENTLY_USED_KEY]: 'history',
activity: 'dumbbell', activity: 'dumbbell',
people: 'smiley', people: 'smiley',
nature: 'nature', nature: 'nature',
......
import Cookies from 'js-cookie';
import { getFrequentlyUsedEmojis, addToFrequentlyUsed } from '~/emoji/components/utils';
jest.mock('js-cookie');
describe('getFrequentlyUsedEmojis', () => {
it('it returns null when no saved emojis set', () => {
jest.spyOn(Cookies, 'get').mockReturnValue(null);
expect(getFrequentlyUsedEmojis()).toBe(null);
});
it('it returns frequently used emojis object', () => {
jest.spyOn(Cookies, 'get').mockReturnValue('thumbsup,thumbsdown');
expect(getFrequentlyUsedEmojis()).toEqual({
frequently_used: {
emojis: [['thumbsup', 'thumbsdown']],
top: 0,
height: 71,
},
});
});
});
describe('addToFrequentlyUsed', () => {
it('sets cookie value', () => {
jest.spyOn(Cookies, 'get').mockReturnValue(null);
addToFrequentlyUsed('thumbsup');
expect(Cookies.set).toHaveBeenCalledWith('frequently_used_emojis', 'thumbsup', {
expires: 365,
});
});
it('sets cookie value to include previously set cookie value', () => {
jest.spyOn(Cookies, 'get').mockReturnValue('thumbsdown');
addToFrequentlyUsed('thumbsup');
expect(Cookies.set).toHaveBeenCalledWith('frequently_used_emojis', 'thumbsdown,thumbsup', {
expires: 365,
});
});
it('sets cookie value with uniq values', () => {
jest.spyOn(Cookies, 'get').mockReturnValue('thumbsup');
addToFrequentlyUsed('thumbsup');
expect(Cookies.set).toHaveBeenCalledWith('frequently_used_emojis', 'thumbsup', {
expires: 365,
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment