[IMP] mail: Improve Emoji Picker (step 2)

Improvements:

- Much more emojis in the emoji picker (more than 1800 emojis, up from 81)
- Fuzzy search bar to find emoji in the emoji picker

Known limitations:

- Emoji visual is based on browser/OS, so some emojis may not be visible on some devices

Task-2957928

closes odoo/odoo#99343

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-09-07 02:17:47 +02:00
parent fc705d423e
commit b5ce1e39d7
50 changed files with 28040 additions and 240 deletions
+1
View File
@@ -117,6 +117,7 @@ For more specific needs, you may also assign custom-defined actions
'mail.assets_messaging': [
'mail/static/src/model/*.js',
'mail/static/src/models/*.js',
'mail/static/src/models_data/*.js',
'mail/static/lib/selfie_segmentation/selfie_segmentation.js',
],
# Custom bundle in case we want to remove things that are later added to web.assets_common
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.Emoji" owl="1">
<span class="o_Emoji p-1 fs-3 cursor-pointer" t-att-class="emojiView.isHovered ? 'bg-200' : ''" t-on-click="emojiView.onClick" t-on-mouseenter="emojiView.onMouseenter" t-on-mouseleave="emojiView.onMouseleave" t-att-title="emojiView.emoji.description" t-att-data-source="emojiView.emoji.sources[0]" t-att-data-unicode="emojiView.emoji.unicode" t-attf-class="{{ className }}" t-ref="root">
<t t-esc="emojiView.emoji.unicode"/>
</span>
</t>
</templates>
@@ -0,0 +1,4 @@
.o_EmojiCategoryView {
filter: grayscale(1);
}
@@ -2,8 +2,8 @@
<templates xml:space="preserve">
<t t-name="mail.EmojiCategoryView" owl="1">
<span class="o_EmojiCategoryView px-2 py-1 fs-5 cursor-pointer" t-att-class="(emojiCategoryView.isHovered or emojiCategoryView.emojiCategoryBarViewOwnerAsActive) ? 'bg-300' : ''" t-on-click="emojiCategoryView.onClick" t-on-mouseenter="emojiCategoryView.onMouseenter" t-on-mouseleave="emojiCategoryView.onMouseleave" t-attf-class="{{ className }}" t-ref="root">
<t t-esc="emojiCategoryView.emojiCategory.categoryName"/>
<span class="o_EmojiCategoryView px-2 py-1 fs-5 cursor-pointer" t-att-class="(emojiCategoryView.isHovered or emojiCategoryView.isActive) ? 'bg-200' : ''" t-att-title="emojiCategoryView.category.name" t-on-click="emojiCategoryView.onClick" t-on-mouseenter="emojiCategoryView.onMouseenter" t-on-mouseleave="emojiCategoryView.onMouseleave" t-attf-class="{{ className }}" t-ref="root">
<t t-esc="emojiCategoryView.category.title"/>
</span>
</t>
@@ -0,0 +1,21 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class EmojiGridItemView extends Component {
/**
* @returns {EmojiGridItemView}
*/
get emojiGridItemView() {
return this.props.record;
}
}
Object.assign(EmojiGridItemView, {
props: { record: Object },
template: 'mail.EmojiGridItemView',
});
registerMessagingComponent(EmojiGridItemView);
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.EmojiGridItemView" owl="1">
<div class="o_EmojiGridItemView d-flex justify-content-center" t-attf-style="width: {{emojiGridItemView.width}}px;">
<EmojiView t-if="emojiGridItemView.emojiView" className="'o_EmojiGrid_emoji w-100 h-100'" record="emojiGridItemView.emojiView"/>
</div>
</t>
</templates>
@@ -0,0 +1,21 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class EmojiGridRowView extends Component {
/**
* @returns {EmojiGridRowView}
*/
get emojiGridRowView() {
return this.props.record;
}
}
Object.assign(EmojiGridRowView, {
props: { record: Object },
template: 'mail.EmojiGridRowView',
});
registerMessagingComponent(EmojiGridRowView);
@@ -0,0 +1,13 @@
.o_EmojiGridRowView {
display: flex;
text-align: center;
height: var(--rowHeight);
user-select: none;
}
.o_EmojiGridRowView_item {
max-width: var(--itemWidth);
width: var(--itemWidth);
cursor: pointer;
margin: 1px;
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.EmojiGridRowView" owl="1">
<div class="o_EmojiGridRowView">
<EmojiGridSectionView t-if="emojiGridRowView.sectionView" record="emojiGridRowView.sectionView"/>
<t t-else="">
<t t-foreach="emojiGridRowView.items" t-as="item" t-key="item">
<EmojiGridItemView className="'o_EmojiGridRowView_item'" record="item"/>
</t>
</t>
</div>
</t>
</templates>
@@ -0,0 +1,21 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class EmojiGridSectionView extends Component {
/**
* @returns {EmojiGridSectionView}
*/
get emojiGridSectionView() {
return this.props.record;
}
}
Object.assign(EmojiGridSectionView, {
props: { record: Object },
template: 'mail.EmojiGridSectionView',
});
registerMessagingComponent(EmojiGridSectionView);
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.EmojiGridSectionView" owl="1">
<div class="o_EmojiGridSectionView d-flex justify-content-center align-items-end mx-2 text-truncate">
<span t-out="emojiGridSectionView.category.name"/>
</div>
</t>
</templates>
@@ -1,10 +1,20 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
const { Component } = owl;
const { Component, onMounted } = owl;
export class EmojiGridView extends Component {
setup() {
useRefToModel({ fieldName: 'containerRef', refName: 'containerRef'});
useRefToModel({ fieldName: 'listRef', refName: 'listRef'});
useRefToModel({ fieldName: 'viewBlockRef', refName: 'viewBlockRef'});
onMounted(() => {
this.emojiGridView.calculateDimensions();
});
}
/**
* @returns {EmojiGridView}
*/
@@ -5,4 +5,22 @@
.o_EmojiGridView {
display: grid;
grid-template-columns: repeat(9, 1fr);
text-align: center;
}
.o_EmojiGridView_container {
height: auto;
width: auto;
overflow-y: scroll;
overflow-x: visible;
}
.o_EmojiGridView_list {
position: relative;
display: flex;
justify-content: flex-start;
}
.o_EmojiGridView_viewBlock {
position: absolute;
}
@@ -2,11 +2,13 @@
<templates xml:space="preserve">
<t t-name="mail.EmojiGridView" owl="1">
<div class="o_EmojiGridView overflow-auto py-2 ps-3 pe-4" t-attf-class="{{ className }}" t-ref="root">
<t t-foreach="emojiGridView.emojiViews" t-as="emojiView" t-key="emojiView.localId">
<Emoji className="'o_EmojiGid_emoji'" record="emojiView"/>
</t>
<div class="o_EmojiGridView o_EmojiGridView_container" t-attf-style="--rowHeight: {{emojiGridView.rowHeight}}px; --itemWidth: {{emojiGridView.itemWidth}}px; height: {{emojiGridView.height}}; width: {{emojiGridView.width}};" t-on-scroll="emojiGridView.onScroll" t-ref="containerRef">
<div class="o_EmojiGridView_list" t-attf-style="height: {{emojiGridView.listHeight}}px; min-height: {{emojiGridView.listHeight}}px;" t-ref="listRef">
<div t-if="emojiGridView.lastRenderedRowIndex >= emojiGridView.firstRenderedRowIndex" class="o_EmojiGridView_viewBlock" t-attf-style="top: {{emojiGridView.distanceFromTop}}px; width: {{emojiGridView.width}};" t-ref="viewBlockRef">
<EmojiGridRowView t-foreach="emojiGridView.renderedRows" t-as="row" t-key="row" record="row"/>
</div>
</div>
</div>
</t>
</templates>
@@ -1,10 +1,14 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
const { Component } = owl;
export class EmojiPickerView extends Component {
setup() {
useComponentToModel({ fieldName: 'component' });
}
/**
* @returns {EmojiPickerView}
*/
@@ -1,3 +0,0 @@
.o_EmojiPickerView {
height: Min(80vh, 340px);
}
@@ -4,6 +4,7 @@
<t t-name="mail.EmojiPickerView" owl="1">
<div class="o_EmojiPickerView d-flex flex-column" t-attf-class="{{ className }}" t-ref="root">
<EmojiCategoryBarView record="emojiPickerView.emojiCategoryBarView"/>
<EmojiSearchBarView record="emojiPickerView.emojiSearchBarView"/>
<EmojiGridView record="emojiPickerView.emojiGridView"/>
</div>
</t>
@@ -0,0 +1,33 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
const { Component } = owl;
export class EmojiSearchBarView extends Component {
/**
* @override
*/
setup() {
super.setup();
useRefToModel({ fieldName: 'inputRef', refName: 'input' });
useUpdateToModel({ methodName: 'onComponentUpdate', modelName: 'EmojiSearchBarView' });
}
/**
* @returns {EmojiSearchBarView}
*/
get emojiSearchBarView() {
return this.props.record;
}
}
Object.assign(EmojiSearchBarView, {
props: { record: Object },
template: 'mail.EmojiSearchBarView',
});
registerMessagingComponent(EmojiSearchBarView);
@@ -0,0 +1,15 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_EmojiSearchBarView_icon.o-isFocused {
color: darken($o-enterprise-primary-color, 5%),
}
.o_EmojiSearchBarView_inputBar.o-isFocused {
color: darken($o-enterprise-primary-color, 5%),
}
.o_EmojiSearchBarView_searchInput:focus {
outline: none;
}
@@ -0,0 +1,14 @@
<templates xml:space="preserve">
<t t-name="mail.EmojiSearchBarView" owl="1">
<div class="o_EmojiSearchBarView overflow-auto d-flex flex-shrink-0 p-2 flex-column" t-attf-class="{{ className }}" t-ref="root">
<div class="o_EmojiSearchBarView_inputContainer d-flex flex-grow-1 align-items-center">
<input class="o_EmojiSearchBarView_searchInput border-0 overflow-auto flex-fill" t-att-class="{ 'opacity-50': !emojiSearchBarView.isFocused }" t-att-placeholder="emojiSearchBarView.placeholder" t-on-focusin="emojiSearchBarView.onFocusinInput" t-on-focusout="emojiSearchBarView.onFocusoutInput" t-on-input="emojiSearchBarView.onInput" t-ref="input"/>
<i class="o_EmojiSearchBarView_icon oi oi-search" t-att-class="{ 'opacity-25': !emojiSearchBarView.isFocused, 'o-isFocused': emojiSearchBarView.isFocused }" title="Search..." role="img" aria-label="Search..."/>
</div>
<hr class="o_EmojiSearchBarView_inputBar my-1" t-att-class="{ 'o-isFocused opacity-75': emojiSearchBarView.isFocused }"/>
</div>
</t>
</templates>
@@ -4,7 +4,7 @@ import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class Emoji extends Component {
export class EmojiView extends Component {
/**
* @returns {EmojiView}
*/
@@ -13,9 +13,9 @@ export class Emoji extends Component {
}
}
Object.assign(Emoji, {
Object.assign(EmojiView, {
props: { record: Object },
template: 'mail.Emoji',
template: 'mail.EmojiView',
});
registerMessagingComponent(Emoji);
registerMessagingComponent(EmojiView);
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.EmojiView" owl="1">
<span class="o_EmojiView p-1 fs-3 cursor-pointer" t-att-class="emojiView.isHovered ? 'bg-200' : ''" t-on-click="emojiView.onClick" t-on-mouseenter="emojiView.onMouseenter" t-on-mouseleave="emojiView.onMouseleave" t-att-title="emojiView.emoji.name" t-att-data-source="emojiView.emoji.sources[0]" t-att-data-codepoints="emojiView.emoji.codepoints" t-attf-class="{{ className }}" t-ref="root">
<t t-esc="emojiView.emoji.codepoints"/>
</span>
</t>
</templates>
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="mail.LegacyEmoji" owl="1">
<span class="o_LegacyEmoji p-1 fs-5 cursor-pointer" t-on-click="props.emojiView.emojiListView.onClickEmoji" t-att-title="props.emojiView.emoji.description" t-att-data-source="props.emojiView.emoji.sources[0]" t-att-data-unicode="props.emojiView.emoji.unicode" t-attf-class="{{ className }}" t-ref="root">
<span class="o_LegacyEmoji p-1 fs-5 cursor-pointer" t-on-click="props.emojiView.emojiListView.onClickEmoji" t-att-title="props.emojiView.emoji.name" t-att-data-source="props.emojiView.emoji.sources[0]" t-att-data-unicode="props.emojiView.emoji.unicode" t-attf-class="{{ className }}" t-ref="root">
<t t-esc="props.emojiView.emoji.unicode"/>
</span>
</t>
@@ -214,7 +214,7 @@ registerModel({
*/
onClickEmoji(ev) {
this.saveStateInStore();
this.insertIntoTextInput(ev.currentTarget.dataset.unicode);
this.insertIntoTextInput(ev.currentTarget.dataset.codepoints);
if (!this.messaging.device.isMobileDevice) {
this.update({ doFocus: true });
}
@@ -1079,7 +1079,7 @@ registerModel({
const regexp = new RegExp(
'(\\s|^)(' + escapedSource + ')(?=\\s|$)',
'g');
htmlString = htmlString.replace(regexp, '$1' + emoji.unicode);
htmlString = htmlString.replace(regexp, '$1' + emoji.codepoints);
}
}
return htmlString;
+68 -9
View File
@@ -2,27 +2,76 @@
import { registerModel } from '@mail/model/model_core';
import { attr, many, one } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
registerModel({
name: 'Emoji',
recordMethods: {
/**
* Compares two strings
*
* @private
* @returns {FieldCommand}
* @returns {boolean}
*/
_computeEmojiRegistry() {
return this.messaging.emojiRegistry;
_fuzzySearch(string, search) {
let i = 0;
let j = 0;
while (i < string.length) {
if (string[i] === search[j]) {
j += 1;
}
if (j === search.length) {
return true;
}
i += 1;
}
return false;
},
/**
* @private
* @returns {boolean}
*/
_isStringInEmojiKeywords(string) {
for (let index in this.searchData) {
if (this._fuzzySearch(this.searchData[index], string)) { //If at least one correspondence is found, return true.
return true;
}
}
return false;
},
},
fields: {
description: attr({
readonly: true,
allEmojiInCategoryOfCurrent: many('EmojiInCategory', {
compute() {
return this.emojiCategories.map(category => ({ category }));
},
inverse: 'emoji',
isCausal: true,
}),
codepoints: attr({
identifying: true,
}),
emojiCategories: many('EmojiCategory', {
compute() {
if (!this.emojiRegistry) {
return clear();
}
return [this.emojiDataCategory];
},
inverse: 'allEmojis',
}),
emojiDataCategory: one('EmojiCategory'),
emojiOrEmojiInCategory: many('EmojiOrEmojiInCategory', {
inverse: 'emoji',
isCausal: true,
}),
emojiRegistry: one('EmojiRegistry', {
compute: '_computeEmojiRegistry',
compute() {
if (!this.messaging) {
return clear();
}
return this.messaging.emojiRegistry;
},
inverse: 'allEmojis',
required: true,
}),
@@ -31,11 +80,21 @@ registerModel({
readonly: true,
isCausal: true,
}),
sources: attr({
emoticons: attr(),
keywords: attr(),
name: attr({
readonly: true,
}),
unicode: attr({
identifying: true,
searchData: attr({
compute() {
return [...this.shortcodes, ...this.emoticons, ...this.name, ...this.keywords];
},
}),
shortcodes: attr(),
sources: attr({
compute() {
return [...this.shortcodes, ...this.emoticons];
},
}),
},
});
@@ -1,20 +1,42 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, many } from '@mail/model/model_field';
import { attr, many, one } from '@mail/model/model_field';
registerModel({
name: 'EmojiCategory',
fields: {
allEmojiInCategoryOfCurrent: many('EmojiInCategory', {
inverse: 'category',
isCausal: true,
}),
allEmojiPickerViewCategory: many('EmojiPickerView.Category', {
inverse: 'category',
isCausal: true,
}),
allEmojis: many('Emoji', {
inverse: 'emojiCategories',
}),
categoryName: attr({
emojiCount: attr({ //Number of emojis that will be in that category once every emoji is loaded.
default: 0,
}),
emojiRegistry: one("EmojiRegistry", {
compute() {
return this.messaging.emojiRegistry;
},
inverse: "allCategories",
required: true,
}),
name: attr({
identifying: true,
}),
emojiCategoryViews: many('EmojiCategoryView', {
inverse: 'emojiCategory',
isCausal: true,
sortId: attr({
readonly: true,
required: true,
}),
title: attr({
readonly: true,
required: true,
}),
},
});
@@ -5,49 +5,11 @@ import { many, one } from '@mail/model/model_field';
registerModel({
name: 'EmojiCategoryBarView',
recordMethods: {
/**
* @private
* @returns {FieldCommand}
*/
_computeActiveCategoryView() {
if (!this.activeByUserCategoryView) {
return this.defaultActiveCategoryView;
}
return this.activeByUserCategoryView;
},
/**
* @private
* @returns {FieldCommand}
*/
_computeDefaultActiveCategoryView() {
return {
emojiCategory: { categoryName: "all" },
emojiCategoryBarViewOwner: this,
};
},
/**
* @private
* @returns {FieldCommand}
*/
_computeEmojiCategoryViews() {
return this.messaging.emojiRegistry.allCategories.map(emojiCategory => ({ emojiCategory }));
},
},
fields: {
activeByUserCategoryView: one('EmojiCategoryView', {
inverse: 'emojiCategoryBarViewOwnerAsActiveByUser',
}),
activeCategoryView: one('EmojiCategoryView', {
compute: '_computeActiveCategoryView',
inverse: 'emojiCategoryBarViewOwnerAsActive',
required: true,
}),
defaultActiveCategoryView: one('EmojiCategoryView', {
compute: '_computeDefaultActiveCategoryView',
}),
emojiCategoryViews: many('EmojiCategoryView', {
compute: '_computeEmojiCategoryViews',
compute() {
return this.emojiPickerViewOwner.categories.map(category => ({ viewCategory: category }));
},
inverse: 'emojiCategoryBarViewOwner',
isCausal: true,
}),
@@ -10,7 +10,16 @@ registerModel({
* @param {MouseEvent} ev
*/
onClick() {
this.update({ emojiCategoryBarViewOwnerAsActiveByUser: this.emojiCategoryBarViewOwner });
this.emojiCategoryBarViewOwner.emojiPickerViewOwner.emojiSearchBarView.reset();
let categoryRowScrollPosition = Math.max(
0,
// Index of the beginning of the category
(this.emojiCategoryBarViewOwner.emojiPickerViewOwner.emojiGridView.rowHeight * this.viewCategory.emojiGridRowView.index)
-
// Cancels the amount of buffer rows
(this.emojiCategoryBarViewOwner.emojiPickerViewOwner.emojiGridView.rowHeight * this.emojiCategoryBarViewOwner.emojiPickerViewOwner.emojiGridView.topBufferAmount)
);
this.emojiCategoryBarViewOwner.emojiPickerViewOwner.emojiGridView.containerRef.el.scrollTo({ top: categoryRowScrollPosition });
},
/**
* @param {MouseEvent} ev
@@ -26,22 +35,24 @@ registerModel({
},
},
fields: {
emojiCategory: one('EmojiCategory', {
identifying: true,
inverse: 'emojiCategoryViews',
category: one('EmojiCategory', {
related: 'viewCategory.category',
}),
emojiCategoryBarViewOwner: one('EmojiCategoryBarView', {
identifying: true,
inverse: 'emojiCategoryViews',
}),
emojiCategoryBarViewOwnerAsActiveByUser: one('EmojiCategoryBarView', {
inverse: 'activeByUserCategoryView',
}),
emojiCategoryBarViewOwnerAsActive: one('EmojiCategoryBarView', {
inverse: 'activeCategoryView',
isActive: attr({
compute() {
return Boolean(this.viewCategory.emojiPickerViewAsActive);
},
}),
isHovered: attr({
default: false,
}),
viewCategory: one('EmojiPickerView.Category', {
identifying: true,
inverse: 'emojiCategoryView',
}),
}
});
@@ -0,0 +1,41 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
registerModel({
name: 'EmojiGridItemView',
fields: {
emojiOrEmojiInCategory: one('EmojiOrEmojiInCategory', {
identifying: true,
inverse: 'emojiGridItemViews',
}),
emojiGridRowViewOwner: one('EmojiGridRowView', {
identifying: true,
inverse: 'items',
}),
emojiView: one('EmojiView', {
compute() {
if (this.emojiOrEmojiInCategory.emoji) {
return { emoji: this.emojiOrEmojiInCategory.emoji };
}
if (this.emojiOrEmojiInCategory.emojiInCategory) {
return { emoji: this.emojiOrEmojiInCategory.emojiInCategory.emoji };
}
return clear();
},
inverse: 'emojiGridItemViewOwner',
isCausal: true,
}),
width: attr({
compute() {
if (!this.emojiGridRowViewOwner.emojiGridViewOwner) {
return clear();
}
return this.emojiGridRowViewOwner.emojiGridViewOwner.itemWidth;
},
default: 0,
}),
},
});
@@ -0,0 +1,38 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, many, one } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
registerModel({
name: 'EmojiGridRowView',
fields: {
emojiGridViewOwner: one('EmojiGridView', {
related: 'emojiGridViewRowRegistryOwner.emojiGridViewOwner',
}),
index: attr({
identifying: true,
}),
items: many('EmojiGridItemView', {
isCausal: true,
inverse: 'emojiGridRowViewOwner',
}),
sectionView: one('EmojiGridSectionView', {
compute() {
if (this.viewCategory) {
return {};
}
return clear();
},
isCausal: true,
inverse: 'emojiGridRowViewOwner',
}),
emojiGridViewRowRegistryOwner: one('EmojiGridViewRowRegistry', {
identifying: true,
inverse: 'rows',
}),
viewCategory: one('EmojiPickerView.Category', {
inverse: 'emojiGridRowView',
}),
},
});
@@ -0,0 +1,20 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
registerModel({
name: 'EmojiGridSectionView',
fields: {
category: one('EmojiCategory', {
related: 'viewCategory.category',
}),
emojiGridRowViewOwner: one('EmojiGridRowView', {
identifying: true,
inverse: 'sectionView',
}),
viewCategory: one('EmojiPickerView.Category', {
related: 'emojiGridRowViewOwner.viewCategory',
}),
},
});
+182 -11
View File
@@ -1,32 +1,203 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { many, one } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
import { attr, many, one } from '@mail/model/model_field';
import { clear, increment } from '@mail/model/model_field_command';
registerModel({
name: 'EmojiGridView',
recordMethods: {
calculateDimensions() {
if (!this.containerRef.el || !this.viewBlockRef.el) {
return;
}
const fittingAmount = this.containerRef.el.clientHeight / this.rowHeight;
const amountToDisplay = fittingAmount + this.additionalRowsToRender;
this.update({
amountToDisplay,
scrollRecomputeCount: increment()
});
},
onScroll() {
this.onScrollThrottle.do();
},
/**
* @private
* @returns {FieldCommand}
* @returns {boolean}
* Filters emoji according to the current search terms.
*/
_computeEmojiViews() {
if (!this.emojiPickerViewOwner.emojiCategoryBarView.activeCategoryView) {
return clear();
}
return this.emojiPickerViewOwner.emojiCategoryBarView.activeCategoryView.emojiCategory.allEmojis.map(emoji => ({ emoji }));
_filterEmoji(emoji) {
return (emoji._isStringInEmojiKeywords(this.emojiPickerViewOwner.emojiSearchBarView.currentSearch));
},
_sortRenderedRows() {
return [
['smaller-first', 'index'],
];
},
},
fields: {
additionalRowsToRender: attr({
default: 4,
}),
amountOfItemsPerRow: attr({
default: 9,
}),
amountToDisplay: attr({
default: 50,
}),
containerRef: attr(),
distanceFromTop: attr({
compute() {
this.scrollRecomputeCount; // observe scroll changes
if (!this.listRef || !this.listRef.el) {
return clear();
}
let distanceFromTopValue;
const buffer = this.topBufferAmount * this.rowHeight;
let offset = this.scrollPercentage * this.listRef.el.clientHeight;
if (offset < buffer) {
distanceFromTopValue = 0;
} else {
distanceFromTopValue = offset - buffer;
}
return distanceFromTopValue;
},
default: 0,
}),
emojiPickerViewOwner: one('EmojiPickerView', {
identifying: true,
inverse: 'emojiGridView',
}),
emojiViews: many('EmojiView', {
compute: '_computeEmojiViews',
inverse: 'emojiGridView',
firstRenderedRowIndex: attr({
compute() {
this.scrollRecomputeCount; // observe scroll changes
let value;
if (this.scrollIndex - this.amountToDisplay >= this.rows.length) {
value = this.scrollIndex - this.amountToDisplay;
} else {
value = this.scrollIndex;
}
return Math.floor(value);
},
default: 0,
}),
height: attr({
default: "275px",
}),
itemWidth: attr({
default: 30,
}),
lastRenderedRowIndex: attr({
compute() {
this.scrollRecomputeCount; // observe scroll changes
let value;
if (this.firstRenderedRowIndex + this.amountToDisplay >= this.rows.length) {
value = Math.max(this.rows.length - 1, 0);
} else {
value = this.firstRenderedRowIndex + this.amountToDisplay;
}
return Math.floor(value);
},
default: 0,
}),
listHeight: attr({
compute() {
return this.rowHeight * this.rows.length;
},
default: 0,
}),
listRef: attr(),
nonSearchRowRegistry: one('EmojiGridViewRowRegistry', {
default: {},
inverse: 'emojiGridViewOwnerAsNonSearch',
isCausal: true,
}),
onScrollActiveCategory: attr({
compute() {
this.scrollRecomputeCount; // observe scroll changes
for (const viewCategory of this.emojiPickerViewOwner.categories) {
const rowIndex = this.firstRenderedRowIndex + this.topBufferAmount;
if (
viewCategory.emojiGridRowView &&
rowIndex >= viewCategory.emojiGridRowView.index &&
rowIndex <= viewCategory.endSectionIndex
) {
this.emojiPickerViewOwner.update({ activeCategoryByGridViewScroll: viewCategory });
break;
}
}
},
}),
onScrollThrottle: one('Throttle', {
compute() {
return { func: () => this.update({ scrollRecomputeCount: increment() }) };
},
inverse: 'emojiGridViewAsOnScroll',
isCausal: true,
}),
renderedRows: many('EmojiGridRowView', {
compute() {
if (this.lastRenderedRowIndex + 1 - this.firstRenderedRowIndex < 0) {
return clear();
}
if (this.rows.length === 0) {
return clear();
}
return (
[...Array(this.lastRenderedRowIndex + 1 - this.firstRenderedRowIndex).keys()]
.map(relativeRowIndex => this.rows[this.firstRenderedRowIndex + relativeRowIndex])
.filter(row => row !== undefined) // some corner cases where very briefly it doesn't sync with rows and it's bigger
);
},
sort: '_sortRenderedRows',
}),
rowHeight: attr({
default: 30,
}),
rows: many('EmojiGridRowView', {
compute() {
if (this.emojiPickerViewOwner.emojiSearchBarView.currentSearch !== "") {
return this.searchRowRegistry.rows;
}
return this.nonSearchRowRegistry.rows;
},
}),
scrollIndex: attr({
compute() {
this.scrollRecomputeCount; // observe scroll changes
return (this.scrollPercentage * this.rows.length);
},
default: 0,
}),
scrollPercentage: attr({
compute() {
this.scrollRecomputeCount; // observe scroll changes
if (!this.containerRef || !this.containerRef.el) {
return clear();
}
return this.containerRef.el.scrollTop / this.containerRef.el.scrollHeight;
},
default: 0,
}),
scrollbarThresholdWidth: attr({
default: 15,
}),
scrollRecomputeCount: attr({
default: 0,
}),
searchRowRegistry: one('EmojiGridViewRowRegistry', {
default: {},
inverse: 'emojiGridViewOwnerAsSearch',
isCausal: true,
}),
topBufferAmount: attr({
default: 2,
}),
viewBlockRef: attr(),
width: attr({
compute() {
return `${this.itemWidth * this.amountOfItemsPerRow + this.scrollbarThresholdWidth}px`;
},
}),
},
});
@@ -0,0 +1,91 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { many, one } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
registerModel({
name: 'EmojiGridViewRowRegistry',
identifyingMode: 'xor',
recordMethods: {
computeNonSearchRows() {
const value = [];
let index = 0;
for (let viewCategory of this.emojiGridViewOwner.emojiPickerViewOwner.categories) {
value.push({ viewCategory, index });
index++;
let currentItems = [];
for (let emojiInCategory of viewCategory.category.allEmojiInCategoryOfCurrent) {
currentItems.push({ emojiOrEmojiInCategory: { emojiInCategory } });
if (currentItems.length === this.emojiGridViewOwner.amountOfItemsPerRow) {
value.push({ items: currentItems, index });
currentItems = [];
index++;
}
}
if (currentItems.length > 0) {
value.push({ items: currentItems, index });
currentItems = [];
index++;
}
viewCategory.update({ endSectionIndex: index - 1 });
}
return value;
},
computeSearchRows() {
if (this.emojiGridViewOwner.emojiPickerViewOwner.emojiSearchBarView.currentSearch === "") {
return clear();
}
const emojis = this.messaging.emojiRegistry.allEmojis.filter(this.emojiGridViewOwner._filterEmoji);
const value = [];
let index = 0;
let currentItems = [];
for (let emoji of emojis) {
currentItems.push({ emojiOrEmojiInCategory: { emoji } });
if (currentItems.length === this.emojiGridViewOwner.amountOfItemsPerRow) {
index++;
value.push({ items: currentItems, index });
currentItems = [];
}
}
return value;
},
_sortRows() {
return [
['smaller-first', 'index'],
];
},
},
fields: {
rows: many('EmojiGridRowView', {
compute() {
if (!this.emojiGridViewOwner) {
return clear();
}
if (this.emojiGridViewOwnerAsNonSearch) {
return this.computeNonSearchRows();
}
if (this.emojiGridViewOwnerAsSearch) {
return this.computeSearchRows();
}
return clear();
},
isCausal: true,
inverse: 'emojiGridViewRowRegistryOwner',
sort: '_sortRows',
}),
emojiGridViewOwner: one('EmojiGridView', {
compute() {
return this.emojiGridViewOwnerAsNonSearch || this.emojiGridViewOwnerAsSearch;
},
}),
emojiGridViewOwnerAsNonSearch: one('EmojiGridView', {
identifying: true,
inverse: 'nonSearchRowRegistry',
}),
emojiGridViewOwnerAsSearch: one('EmojiGridView', {
identifying: true,
inverse: 'searchRowRegistry',
}),
},
});
@@ -0,0 +1,22 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { many, one } from '@mail/model/model_field';
registerModel({
name: 'EmojiInCategory',
fields: {
category: one('EmojiCategory', {
identifying: true,
inverse: 'allEmojiInCategoryOfCurrent',
}),
emoji: one('Emoji', {
identifying: true,
inverse: 'allEmojiInCategoryOfCurrent',
}),
emojiOrEmojiInCategory: many('EmojiOrEmojiInCategory', {
inverse: 'emojiInCategory',
isCausal: true,
}),
},
});
@@ -0,0 +1,23 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { many, one } from '@mail/model/model_field';
registerModel({
name: 'EmojiOrEmojiInCategory',
identifyingMode: 'xor',
fields: {
emoji: one('Emoji', {
identifying: true,
inverse: 'emojiOrEmojiInCategory',
}),
emojiInCategory: one('EmojiInCategory', {
identifying: true,
inverse: 'emojiOrEmojiInCategory',
}),
emojiGridItemViews: many('EmojiGridItemView', {
isCausal: true,
inverse: 'emojiOrEmojiInCategory',
}),
},
});
@@ -1,11 +1,43 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
import { attr, many, one } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
registerModel({
name: 'EmojiPickerView',
fields: {
activeCategoryByGridViewScroll: one('EmojiPickerView.Category'),
activeCategory: one('EmojiPickerView.Category', {
compute() {
if (this.emojiSearchBarView.currentSearch !== "") {
return clear();
}
if (this.activeCategoryByGridViewScroll) {
return this.activeCategoryByGridViewScroll;
}
if (this.defaultActiveCategory) {
return this.defaultActiveCategory;
}
return clear();
},
inverse: 'emojiPickerViewAsActive',
}),
categories: many('EmojiPickerView.Category', {
compute() {
return this.messaging.emojiRegistry.allCategories.map(category => ({ category }));
},
inverse: 'emojiPickerViewOwner',
isCausal: true,
}),
defaultActiveCategory: one('EmojiPickerView.Category', {
compute() {
if (this.categories.length === 0) {
return clear();
}
return this.categories[0];
},
}),
emojiCategoryBarView: one('EmojiCategoryBarView', {
default: {},
inverse: 'emojiPickerViewOwner',
@@ -20,9 +52,16 @@ registerModel({
required: true,
isCausal: true,
}),
emojiSearchBarView: one('EmojiSearchBarView', {
default: {},
inverse: 'emojiPickerView',
readonly: true,
isCausal: true,
}),
popoverViewOwner: one('PopoverView', {
identifying: true,
inverse: 'emojiPickerView',
}),
component: attr(),
},
});
@@ -0,0 +1,34 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
/**
* Emoji category info of a specific emoji picker view
*/
registerModel({
name: 'EmojiPickerView.Category',
fields: {
category: one('EmojiCategory', {
identifying: true,
inverse: 'allEmojiPickerViewCategory',
}),
emojiPickerViewOwner: one('EmojiPickerView', {
identifying: true,
inverse: 'categories',
}),
emojiPickerViewAsActive: one('EmojiPickerView', {
inverse: 'activeCategory',
}),
emojiCategoryView: one('EmojiCategoryView', {
inverse: 'viewCategory',
isCausal: true,
}),
emojiGridRowView: one('EmojiGridRowView', {
inverse: 'viewCategory',
}),
endSectionIndex: attr({
default: 0,
}),
},
});
+60 -96
View File
@@ -1,107 +1,71 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { many } from '@mail/model/model_field';
import { attr, many } from '@mail/model/model_field';
import { insert } from '@mail/model/model_field_command';
import { emojiCategoriesData, emojisData } from '@mail/models_data/emoji_data';
import { executeGracefully } from '@mail/utils/utils';
registerModel({
name: 'EmojiRegistry',
lifecycleHooks: {
_created() {
this._populateFromEmojiData(emojiCategoriesData, emojisData);
},
},
recordMethods: {
async _populateFromEmojiData(dataCategories, dataEmojis) {
await executeGracefully(dataCategories.map(category => () => {
if (!this.exists()) {
return;
}
const emojiCount = dataEmojis.reduce((acc, emoji) => emoji.category === category.name ? acc + 1 : acc, 0);
this.update({
dataCategories: insert({
name: category.name,
title: category.title,
sortId: category.sortId,
emojiCount,
}),
});
}));
this.update({ allEmojiCount: dataEmojis.length }); // Set the total number of emojis to the category before all emojis are processed and created.
await executeGracefully(dataEmojis.map(emojiData => () => {
if (!this.exists()) {
return;
}
this.models['Emoji'].insert({
codepoints: emojiData.codepoints,
shortcodes: emojiData.shortcodes,
emoticons: emojiData.emoticons,
name: emojiData.name,
keywords: emojiData.keywords,
emojiDataCategory: { name: emojiData.category },
});
}));
},
_sortAllCategories() {
return [['smaller-first', 'sortId']];
},
_sortAllEmojis() {
return [['smaller-first', 'codepoints']];
}
},
fields: {
allEmojis: many('Emoji', {
default: [
{ unicode: "😊", sources: [":)", ":-)", "=)", ":]"], description: ":)" },
{ unicode: "😃", sources: [":D", ":-D", "=D"], description: ":D" },
{ unicode: "😆", sources: ["xD", "XD"], description: "xD" },
{ unicode: "😂", sources: ["x'D"], description: "x'D" },
{ unicode: "😉", sources: [";)", ";-)"], description: ";)" },
{ unicode: "😎", sources: ["B)", "8)", "B-)", "8-)"], description: "B)" },
{ unicode: "😜", sources: [";p", ";P"], description: ";p" },
{ unicode: "😋", sources: [":p", ":P", ":-p", ":-P", "=P"], description: ":p" },
{ unicode: "😝", sources: ["xp", "xP"], description: "xp" },
{ unicode: "😳", sources: ["o_o"], description: "o_o" },
{ unicode: "😐", sources: [":|", ":-|"], description: ":|" },
{ unicode: "😕", sources: [":/", ":-/"], description: ":/" },
{ unicode: "😞", sources: [":("], description: ":(" },
{ unicode: "😱", sources: [":@"], description: ":@" },
{ unicode: "😲", sources: [":O", ":-O", ":o", ":-o"], description: ":O" },
{ unicode: "😨", sources: [":'o"], description: ":'o" },
{ unicode: "😠", sources: ["3:(", ">:("], description: "3:(" },
{ unicode: "😈", sources: ["3:)", ">:)"], description: "3:)" },
{ unicode: "😘", sources: [":*", ":-*"], description: ":*" },
{ unicode: "😇", sources: ["o:)"], description: "o:)" },
{ unicode: "😢", sources: [":'("], description: ":'(" },
{ unicode: "😭", sources: [":'-(", ":\"("], description: ":'-(" },
{ unicode: "❤️", sources: ["<3", "&lt;3", ":heart"], description: "<3" },
{ unicode: "💔", sources: ["</3", "&lt;/3"], description: "</3" },
{ unicode: "😍", sources: [":heart_eyes"], description: ":heart_eyes" },
{ unicode: "👳", sources: [":turban"], description: ":turban" },
{ unicode: "👍", sources: [":+1"], description: ":+1" },
{ unicode: "👎", sources: [":-1"], description: ":-1" },
{ unicode: "👌", sources: [":ok"], description: ":ok" },
{ unicode: "💩", sources: [":poop"], description: ":poop" },
{ unicode: "🙈", sources: [":no_see"], description: ":no_see" },
{ unicode: "🙉", sources: [":no_hear"], description: ":no_hear" },
{ unicode: "🙊", sources: [":no_speak"], description: ":no_speak" },
{ unicode: "🐞", sources: [":bug"], description: ":bug" },
{ unicode: "😺", sources: [":kitten"], description: ":kitten" },
{ unicode: "🐻", sources: [":bear"], description: ":bear" },
{ unicode: "🐌", sources: [":snail"], description: ":snail" },
{ unicode: "🐗", sources: [":boar"], description: ":boar" },
{ unicode: "🍀", sources: [":clover"], description: ":clover" },
{ unicode: "🌹", sources: [":sunflower"], description: ":sunflower" },
{ unicode: "🔥", sources: [":fire"], description: ":fire" },
{ unicode: "☀️", sources: [":sun"], description: ":sun" },
{ unicode: "⛅️", sources: [":partly_sunny"], description: ":partly_sunny" },
{ unicode: "🌈", sources: [":rainbow"], description: ":rainbow" },
{ unicode: "☁️", sources: [":cloud"], description: ":cloud" },
{ unicode: "⚡️", sources: [":zap"], description: ":zap" },
{ unicode: "⭐️", sources: [":star"], description: ":star" },
{ unicode: "🍪", sources: [":cookie"], description: ":cookie" },
{ unicode: "🍕", sources: [":pizza"], description: ":pizza" },
{ unicode: "🍔", sources: [":hamburger"], description: ":hamburger" },
{ unicode: "🍟", sources: [":fries"], description: ":fries" },
{ unicode: "🎂", sources: [":cake"], description: ":cake" },
{ unicode: "🍰", sources: [":cake_part"], description: ":cake_part" },
{ unicode: "☕️", sources: [":coffee"], description: ":coffee" },
{ unicode: "🍌", sources: [":banana"], description: ":banana" },
{ unicode: "🍣", sources: [":sushi"], description: ":sushi" },
{ unicode: "🍙", sources: [":rice_ball"], description: ":rice_ball" },
{ unicode: "🍺", sources: [":beer"], description: ":beer" },
{ unicode: "🍷", sources: [":wine"], description: ":wine" },
{ unicode: "🍸", sources: [":cocktail"], description: ":cocktail" },
{ unicode: "🍹", sources: [":tropical"], description: ":tropical" },
{ unicode: "🍻", sources: [":beers"], description: ":beers" },
{ unicode: "👻", sources: [":ghost"], description: ":ghost" },
{ unicode: "💀", sources: [":skull"], description: ":skull" },
{ unicode: "👽", sources: [":et", ":alien"], description: ":et" },
{ unicode: "🎉", sources: [":party"], description: ":party" },
{ unicode: "🏆", sources: [":trophy"], description: ":trophy" },
{ unicode: "🔑", sources: [":key"], description: ":key" },
{ unicode: "📌", sources: [":pin"], description: ":pin" },
{ unicode: "📯", sources: [":postal_horn"], description: ":postal_horn" },
{ unicode: "🎵", sources: [":music"], description: ":music" },
{ unicode: "🎺", sources: [":trumpet"], description: ":trumpet" },
{ unicode: "🎸", sources: [":guitar"], description: ":guitar" },
{ unicode: "🏃", sources: [":run"], description: ":run" },
{ unicode: "🚲", sources: [":bike"], description: ":bike" },
{ unicode: "⚽️", sources: [":soccer"], description: ":soccer" },
{ unicode: "🏈", sources: [":football"], description: ":football" },
{ unicode: "🎱", sources: [":8ball"], description: ":8ball" },
{ unicode: "🎬", sources: [":clapper"], description: ":clapper" },
{ unicode: "🎤", sources: [":microphone"], description: ":microphone" },
{ unicode: "🧀", sources: [":cheese"], description: ":cheese" },
],
inverse: 'emojiRegistry',
}),
allCategories: many('EmojiCategory', {
default: [
{ categoryName: "all", allEmojis: ["😊", "😃", "😆", "😂", "😉", "😎", "😜", "😋", "😝", "😳", "😐", "😕", "😞", "😱", "😲", "😨", "😠", "😈", "😘", "😇", "😢", "😭", "❤️", "💔", "😍", "👳", "👍", "👎", "👌", "💩", "🙈", "🙉", "🙊", "🐞", "😺", "🐻", "🐌", "🐗", "🍀", "🌹", "🔥", "☀️", "⛅️", "🌈", "☁️", "⚡️", "⭐️", "🍪", "🍕", "🍔", "🍟", "🎂", "🍰", "☕️", "🍌", "🍣", "🍙", "🍺", "🍷", "🍸", "🍹", "🍻", "🧀", "👻", "💀", "👽", "🎉", "🏆", "🔑", "📌", "📯", "🎵", "🎺", "🎸", "🏃", "🚲", "⚽️", "🏈", "🎱", "🎬", "🎤"].map(unicode => ({ unicode })) },
{ categoryName: "🤠", allEmojis: ["😊", "😃", "😆", "😂", "😉", "😎", "😜", "😋", "😝", "😳", "😐", "😕", "😞", "😱", "😲", "😨", "😠", "😈", "😘", "😇", "😢", "😭", "❤️", "💔", "😍", "👳", "👍", "👎", "👌", "💩"].map(unicode => ({ unicode })) },
{ categoryName: "🐿", allEmojis: ["🙈", "🙉", "🙊", "🐞", "😺", "🐻", "🐌", "🐗"].map(unicode => ({ unicode })) },
{ categoryName: "🌿", allEmojis: ["🍀", "🌹", "🔥", "☀️", "⛅️", "🌈", "☁️", "⚡️", "⭐️"].map(unicode => ({ unicode })) },
{ categoryName: "🍽", allEmojis: ["🍪", "🍕", "🍔", "🍟", "🎂", "🍰", "☕️", "🍌", "🍣", "🍙", "🍺", "🍷", "🍸", "🍹", "🍻", "🧀"].map(unicode => ({ unicode })) },
{ categoryName: "📦", allEmojis: ["👻", "💀", "👽", "🎉", "🏆", "🔑", "📌", "📯", "🎵", "🎺", "🎸"].map(unicode => ({ unicode })) },
{ categoryName: "⚽️", allEmojis: ["🏃", "🚲", "⚽️", "🏈", "🎱", "🎬", "🎤"].map(unicode => ({ unicode })) },
],
compute() {
return this.dataCategories;
},
inverse: 'emojiRegistry',
sort: '_sortAllCategories',
}),
allEmojiCount: attr({
default: 0,
}),
allEmojis: many('Emoji', {
inverse: 'emojiRegistry',
sort: '_sortAllEmojis'
}),
dataCategories: many('EmojiCategory'),
},
});
@@ -0,0 +1,81 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
registerModel({
name: 'EmojiSearchBarView',
recordMethods: {
/**
* Handles OWL update on this EmojiSearchBarView component.
*/
onComponentUpdate() {
this._handleFocus();
},
onFocusinInput() {
if (!this.exists()) {
return;
}
this.update({ isFocused: true });
},
onFocusoutInput() {
if (!this.exists()) {
return;
}
this.update({ isFocused: false });
},
/**
* @public
*/
onInput() {
if (!this.exists()) {
return;
}
this.update({
currentSearch: this.inputRef.el.value,
});
},
/**
* @public
*/
reset() {
this.update({ currentSearch: "" });
this.inputRef.el.value = "";
this.update({ isDoFocus: true });
},
/**
* @private
*/
_handleFocus() {
if (this.isDoFocus) {
if (!this.inputRef.el) {
return;
}
this.update({ isDoFocus: false });
this.inputRef.el.focus();
}
},
},
fields: {
currentSearch: attr({
default: "",
}),
emojiPickerView: one("EmojiPickerView", {
identifying: true,
inverse: "emojiSearchBarView",
}),
inputRef: attr(),
isDoFocus: attr({
default: true,
}),
isFocused: attr({
default: false,
}),
placeholder: attr({
compute() {
return (this.env._t("Search here to find an emoji"));
},
required: true,
}),
},
});
+15 -6
View File
@@ -10,12 +10,15 @@ registerModel({
* @param {MouseEvent} ev
*/
onClick(ev) {
if (this.emojiGridView.emojiPickerViewOwner.popoverViewOwner.messageActionViewOwnerAsReaction) {
this.emojiGridView.emojiPickerViewOwner.popoverViewOwner.messageActionViewOwnerAsReaction.onClickReaction(ev);
if (!this.emojiGridItemViewOwner.emojiGridRowViewOwner) {
return;
}
if (this.emojiGridView.emojiPickerViewOwner.popoverViewOwner.composerViewOwnerAsEmoji) {
this.emojiGridView.emojiPickerViewOwner.popoverViewOwner.composerViewOwnerAsEmoji.onClickEmoji(ev);
if (this.emojiGridItemViewOwner.emojiGridRowViewOwner.emojiGridViewOwner.emojiPickerViewOwner.popoverViewOwner.messageActionViewOwnerAsReaction) {
this.emojiGridItemViewOwner.emojiGridRowViewOwner.emojiGridViewOwner.emojiPickerViewOwner.popoverViewOwner.messageActionViewOwnerAsReaction.onClickReaction(ev);
return;
}
if (this.emojiGridItemViewOwner.emojiGridRowViewOwner.emojiGridViewOwner.emojiPickerViewOwner.popoverViewOwner.composerViewOwnerAsEmoji) {
this.emojiGridItemViewOwner.emojiGridRowViewOwner.emojiGridViewOwner.emojiPickerViewOwner.popoverViewOwner.composerViewOwnerAsEmoji.onClickEmoji(ev);
return;
}
},
@@ -23,12 +26,18 @@ registerModel({
* @param {MouseEvent} ev
*/
onMouseenter(ev) {
if (!this.exists()) {
return;
}
this.update({ isHovered: true });
},
/**
* @param {MouseEvent} ev
*/
onMouseleave(ev) {
if (!this.exists()) {
return;
}
this.update({ isHovered: false });
},
},
@@ -37,9 +46,9 @@ registerModel({
identifying: true,
inverse: 'emojiViews',
}),
emojiGridView: one('EmojiGridView', {
emojiGridItemViewOwner: one('EmojiGridItemView', {
identifying: true,
inverse: 'emojiViews',
inverse: 'emojiView',
}),
isHovered: attr({
default: false,
+1 -22
View File
@@ -545,29 +545,8 @@ registerModel({
// body null in db, body will be false instead of empty string
return clear();
}
let prettyBody = this.body;
for (const emoji of this.messaging.emojiRegistry.allEmojis) {
const { unicode } = emoji;
const regexp = new RegExp(
`(?:^|\\s|<[a-z]*>)(${unicode})(?=\\s|$|</[a-z]*>)`,
"g"
);
const originalBody = this.body;
prettyBody = this.body.replace(
regexp,
` <span class="o_mail_emoji">${unicode}</span> `
);
// Idiot-proof limit. If the user had the amazing idea of
// copy-pasting thousands of emojis, the image rendering can lead
// to memory overflow errors on some browsers (e.g. Chrome). Set an
// arbitrary limit to 200 from which we simply don't replace them
// (anyway, they are already replaced by the unicode counterpart).
if (_.str.count(prettyBody, "o_mail_emoji") > 200) {
prettyBody = originalBody;
}
}
// add anchor tags to urls
return parseAndTransform(prettyBody, addLink);
return parseAndTransform(this.body, addLink);
},
/**
* @private
@@ -47,7 +47,7 @@ registerModel({
* @param {MouseEvent} ev
*/
onClickReaction(ev) {
this.messageAction.messageActionListOwner.message.addReaction(ev.currentTarget.dataset.unicode);
this.messageAction.messageActionListOwner.message.addReaction(ev.currentTarget.dataset.codepoints);
this.update({ reactionPopoverView: clear() });
},
/**
@@ -24,7 +24,7 @@ registerModel({
* @returns {boolean}
*/
contains(element) {
return Boolean(this.component && this.component.root.el.contains(element));
return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element));
},
/**
* @private
@@ -47,6 +47,9 @@ registerModel({
* @returns {integer|FieldCommand}
*/
_computeDuration() {
if (this.emojiGridViewAsOnScroll) {
return 150;
}
if (this.threadAsThrottleNotifyCurrentPartnerTypingStatus) {
return 2.5 * 1000;
}
@@ -65,6 +68,10 @@ registerModel({
compute: '_computeDuration',
required: true,
}),
emojiGridViewAsOnScroll: one('EmojiGridView', {
identifying: true,
inverse: 'onScrollThrottle',
}),
/**
* Inner function to be invoked and throttled.
*/
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -33,7 +33,7 @@ async function executeGracefully(functions, maxTimeFrame = 100) {
let startDate = new Date();
for (const func of functions) {
if (new Date() - startDate > maxTimeFrame) {
await new Promise(resolve => setTimeout(resolve));
await new Promise(resolve => setTimeout(resolve, 50));
startDate = new Date();
}
await func();
+2 -2
View File
@@ -3,8 +3,8 @@
<div t-name="mail.legacy.Composer.emojis" class="o_mail_emoji_container">
<t t-foreach="emojis" t-as="emoji">
<span t-att-data-emoji="emoji.sources[0]" class="o_mail_emoji" t-att-title="emoji.description" t-att-aria-label="emoji.description">
<t t-out="emoji.unicode"/>
<span t-att-data-emoji="emoji.sources[0]" class="o_mail_emoji" t-att-title="emoji.name" t-att-aria-label="emoji.name">
<t t-out="emoji.codepoints"/>
</span>
</t>
</div>
@@ -0,0 +1,100 @@
/** @odoo-module **/
import { patchRecordMethods } from '@mail/model/model_core';
// ensure that the model definition is loaded before the patch
import '@mail/models/throttle';
patchRecordMethods('EmojiRegistry', {
async _populateFromEmojiData() {
const dataEmojiCategories = [
{
"name": "Smileys & Emotion",
"title": "🤠",
"sortId": 1
},
{
"name": "People & Body",
"title": "🤟",
"sortId": 2
}];
const dataEmojis = [
{
"codepoints": "😀",
"name": "grinning face",
"shortcodes": [
":grinning:"
],
"emoticons": [],
"category": "Smileys & Emotion",
"keywords": [
"face",
"grin",
"grinning face"
]
},
{
"codepoints": "🤣",
"name": "rolling on the floor laughing",
"shortcodes": [
":rofl:"
],
"emoticons": [],
"category": "Smileys & Emotion",
"keywords": [
"face",
"floor",
"laugh",
"rofl",
"rolling",
"rolling on the floor laughing",
"rotfl"
]
},
{
"codepoints": "😊",
"name": "smiling face with smiling eyes",
"shortcodes": [
":smiling_face_with_smiling_eyes:"
],
"emoticons": [],
"category": "Smileys & Emotion",
"keywords": [
"blush",
"eye",
"face",
"smile",
"smiling face with smiling eyes"
]
},
{
"codepoints": "👋",
"name": "waving hand",
"shortcodes": [
":waving_hand:"
],
"emoticons": [],
"category": "People & Body",
"keywords": [
"hand",
"wave",
"waving"
]
},
{
"codepoints": "🤚",
"name": "raised back of hand",
"shortcodes": [
":raised_back_of_hand:"
],
"emoticons": [],
"category": "People & Body",
"keywords": [
"backhand",
"raised",
"raised back of hand"
]
},
];
this._super(dataEmojiCategories, dataEmojis);
},
});
@@ -182,7 +182,7 @@ QUnit.test('add an emoji', async function (assert) {
});
await openDiscuss();
await click('.o_Composer_buttonEmojis');
await click('.o_Emoji[data-unicode="😊"]');
await click('.o_EmojiView[data-codepoints="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value,
"😊",
@@ -209,7 +209,7 @@ QUnit.test('add an emoji after some text', async function (assert) {
);
await click('.o_Composer_buttonEmojis');
await click('.o_Emoji[data-unicode="😊"]');
await click('.o_EmojiView[data-codepoints="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value,
"Blabla😊",
@@ -239,7 +239,7 @@ QUnit.test('add emoji replaces (keyboard) text selection', async function (asser
// simulate selection of all the content by keyboard
composerTextInputTextArea.setSelectionRange(0, composerTextInputTextArea.value.length);
await click('.o_Composer_buttonEmojis');
await click('.o_Emoji[data-unicode="😊"]');
await click('.o_EmojiView[data-codepoints="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value,
"😊",
@@ -391,7 +391,7 @@ QUnit.test('add an emoji after a canned response', async function (assert) {
// select emoji
await click('.o_Composer_buttonEmojis');
await click('.o_Emoji[data-unicode="😊"]');
await click('.o_EmojiView[data-codepoints="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"Hello! How are you? 😊",
@@ -539,7 +539,7 @@ QUnit.test('add an emoji after a channel mention', async function (assert) {
// select emoji
await click('.o_Composer_buttonEmojis');
await click('.o_Emoji[data-unicode="😊"]');
await click('.o_EmojiView[data-codepoints="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"#General 😊",
@@ -718,7 +718,7 @@ QUnit.test('add an emoji after a command', async function (assert) {
// select emoji
await click('.o_Composer_buttonEmojis');
await click('.o_Emoji[data-unicode="😊"]');
await click('.o_EmojiView[data-codepoints="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"/who 😊",
@@ -899,7 +899,7 @@ QUnit.test('add an emoji after a partner mention', async function (assert) {
// select emoji
await click('.o_Composer_buttonEmojis');
await click('.o_Emoji[data-unicode="😊"]');
await click('.o_EmojiView[data-codepoints="😊"]');
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
"@TestPartner 😊",
@@ -282,7 +282,7 @@ QUnit.test('reply: discard on click away', async function (assert) {
"emoji list should be opened after clicking on emojis button"
);
await click(`.o_Emoji`);
await click(`.o_EmojiView`);
assert.containsNone(
document.body,
'.o_EmojiPickerView',