Commit 1cf9a936 authored by JC Brand's avatar JC Brand

Move Views associated to emojis into a new plugin

parent d7ce231c
......@@ -283,6 +283,10 @@ body.converse-fullscreen {
margin: 0;
}
a {
cursor: pointer;
}
a, a:visited, a:not([href]):not([tabindex]) {
text-decoration: none;
color: var(--link-color);
......
......@@ -16,6 +16,8 @@
padding-bottom: 0;
background-color: var(--chat-head-color);
.emoji-picker__container {
display: flex;
flex-direction: column;
overflow-y: hidden;
background: white;
.emoji-picker__lists {
......@@ -29,13 +31,18 @@
flex-direction: column;
}
.emoji-skintone-picker {
padding: 0.25em 0;
display: flex;
label {
margin: 0;
padding: 0 0.5em;
white-space: nowrap;
font-size: var(--font-size);
color: var(--heading-color);
}
padding: 0.5em 0;
background-color: var(--chat-head-color);
width: auto;
font-size: var(--font-size-huge);
&:hover {
background-color: var(--highlight-color);
}
font-size: var(--font-size-large);
}
}
.emoji-picker {
......@@ -114,9 +121,6 @@
#conversejs.converse-overlayed {
.emoji-picker__container {
height: var(--embedded-emoji-picker-height);
.emoji-picker__lists {
height: calc(var(--embedded-emoji-picker-height) - 4em;
}
}
}
......@@ -152,9 +156,6 @@
#conversejs.converse-fullscreen {
.emoji-picker__container {
height: var(--fullpage-emoji-picker-height);
.emoji-picker__lists {
height: calc(var(--fullpage-emoji-picker-height) - 4em;
}
}
.chatbox {
.sendXMPPMessage {
......
......@@ -153,7 +153,7 @@ $mobile_portrait_length: 480px !default;
--occupants-border-bottom: 1px solid lightgrey;
--occupants-features-display: block;
--embedded-emoji-picker-height: 200px;
--embedded-emoji-picker-height: 300px;
--avatar-border-radius: 10%;
--avatar-border: 1px solid lightgrey;
......@@ -162,14 +162,14 @@ $mobile_portrait_length: 480px !default;
--fullpage-chat-head-height: 62px;
--fullpage-chat-height: calc(var(--vh, 1vh) * 100);
--fullpage-chat-width: 100%;
--fullpage-emoji-picker-height: 200px;
--fullpage-emoji-picker-height: 300px;
--fullpage-max-chat-textarea-height: 15em;
--overlayed-chat-head-height: 55px;
--overlayed-chat-height: 450px;
--overlayed-chat-width: 250px;
--overlayed-chatbox-hover-height: 1em;
--overlayed-emoji-picker-height: 100px;
--overlayed-emoji-picker-height: 150px;
--overlayed-max-chat-textarea-height: 200px;
--overlayed-badge-color: #818479; // $gray-color
......
......@@ -451,18 +451,15 @@
const view = _converse.chatboxviews.get(contact_jid);
const toolbar = view.el.querySelector('ul.chat-toolbar');
expect(toolbar.querySelectorAll('li.toggle-smiley').length).toBe(1);
// Register spies
spyOn(view, 'toggleEmojiMenu').and.callThrough();
spyOn(view, 'insertEmoji').and.callThrough();
view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
toolbar.querySelector('li.toggle-smiley').click();
await u.waitUntil(() => u.isVisible(view.el.querySelector('.toggle-smiley .emoji-picker-container')));
const picker = view.el.querySelector('.toggle-smiley .emoji-picker-container');
const items = picker.querySelectorAll('.emoji-picker li');
items[0].click()
expect(view.insertEmoji).toHaveBeenCalled();
await u.waitUntil(() => u.isVisible(view.el.querySelector('.toggle-smiley .emoji-picker__container')));
const picker = await u.waitUntil(() => view.el.querySelector('.toggle-smiley .emoji-picker__container'));
const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji'));
item.click()
expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':grinning: ');
toolbar.querySelector('li.toggle-smiley').click(); // Close the panel again
done();
......@@ -485,10 +482,9 @@
expect(counter.textContent).toBe('188');
toolbar.querySelector('li.toggle-smiley').click();
await u.waitUntil(() => u.isVisible(view.el.querySelector('.toggle-smiley .emoji-picker-container')));
var picker = view.el.querySelector('.toggle-smiley .emoji-picker-container');
var items = picker.querySelectorAll('.emoji-picker li');
items[0].click()
const picker = await u.waitUntil(() => view.el.querySelector('.toggle-smiley .emoji-picker__container'));
const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji'));
item.click()
expect(counter.textContent).toBe('177');
const textarea = view.el.querySelector('.chat-textarea');
......
......@@ -7,20 +7,16 @@
/**
* @module converse-chatview
*/
import "@converse/headless/converse-emoji";
import "backbone.nativeview";
import "converse-chatboxviews";
import "converse-message-view";
import "converse-modal";
import * as twemoji from "twemoji";
import BrowserStorage from "backbone.browserStorage";
import { Overview } from "backbone.overview";
import bootstrap from "bootstrap.native";
import converse from "@converse/headless/converse-core";
import tpl_chatbox from "templates/chatbox.html";
import tpl_chatbox_head from "templates/chatbox_head.html";
import tpl_chatbox_message_form from "templates/chatbox_message_form.html";
import tpl_emojis from "templates/emojis.html";
import tpl_error_message from "templates/error_message.html";
import tpl_help_message from "templates/help_message.html";
import tpl_info from "templates/info.html";
......@@ -49,7 +45,6 @@ converse.plugins.add('converse-chatview', {
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: [
"converse-emoji",
"converse-chatboxviews",
"converse-disco",
"converse-message-view",
......@@ -65,20 +60,16 @@ converse.plugins.add('converse-chatview', {
_converse.api.settings.update({
'auto_focus': true,
'emoji_image_path': twemoji.default.base,
'message_limit': 0,
'show_send_button': false,
'show_toolbar': true,
'time_format': 'HH:mm',
'use_system_emojis': true,
'visible_toolbar_buttons': {
'call': false,
'clear': true,
'emoji': true,
'spoiler': true
},
});
twemoji.default.base = _converse.emoji_image_path;
function onWindowStateChanged (data) {
if (_converse.chatboxviews) {
......@@ -92,87 +83,6 @@ converse.plugins.add('converse-chatview', {
_converse.api.listen.on('windowStateChanged', onWindowStateChanged);
_converse.EmojiPicker = Backbone.Model.extend({
defaults: {
'current_category': 'people',
'current_skintone': '',
'scroll_position': 0
}
});
_converse.EmojiPickerView = Backbone.VDOMView.extend({
className: 'emoji-picker-container',
events: {
'click .emoji-category-picker li.emoji-category': 'chooseCategory',
'click .emoji-skintone-picker li.emoji-skintone': 'chooseSkinTone'
},
initialize () {
this.model.on('change:current_skintone', this.render, this);
this.model.on('change:current_category', this.render, this);
_converse.api.trigger('emojiPickerViewInitialized');
},
toHTML () {
const html = tpl_emojis(
Object.assign(
this.model.toJSON(), {
'_': _,
'_converse': _converse,
'emoji_categories': _converse.emoji_categories,
'emojis_by_category': u.getEmojisByCategory(),
'shouldBeHidden': this.shouldBeHidden,
'skintones': ['tone1', 'tone2', 'tone3', 'tone4', 'tone5'],
'toned_emojis': _converse.emojis.toned,
'transform': u.getEmojiRenderer()
}
)
);
return html;
},
shouldBeHidden (shortname, current_skintone, toned_emojis) {
/* Helper method for the template which decides whether an
* emoji should be hidden, based on which skin tone is
* currently being applied.
*/
if (_.includes(shortname, '_tone')) {
if (!current_skintone || !_.includes(shortname, current_skintone)) {
return true;
}
} else {
if (current_skintone && _.includes(toned_emojis, shortname)) {
return true;
}
}
return false;
},
chooseSkinTone (ev) {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target.nodeName === 'IMG' ?
ev.target.parentElement : ev.target;
const skintone = target.getAttribute("data-skintone").trim();
if (this.model.get('current_skintone') === skintone) {
this.model.save({'current_skintone': ''});
} else {
this.model.save({'current_skintone': skintone});
}
},
chooseCategory (ev) {
const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
const category = target.getAttribute("data-category").trim();
this.model.save({
'current_category': category,
'scroll_position': 0
});
}
});
_converse.ChatBoxHeading = _converse.ViewWithAvatar.extend({
initialize () {
this.model.on('change:status', this.onStatusMessageChanged, this);
......@@ -336,8 +246,6 @@ converse.plugins.add('converse-chatview', {
'click .toggle-call': 'toggleCall',
'click .toggle-clear': 'clearMessages',
'click .toggle-compose-spoiler': 'toggleComposeSpoilerMessage',
'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
'click .toggle-smiley': 'toggleEmojiMenu',
'click .upload-file': 'toggleFileUpload',
'input .chat-textarea': 'inputChanged',
'keydown .chat-textarea': 'onKeyDown',
......@@ -361,9 +269,6 @@ converse.plugins.add('converse-chatview', {
this.model.presence.on('change:show', this.onPresenceChanged, this);
this.render();
this.createEmojiPicker();
this.insertEmojiPicker();
await this.renderEmojiPicker();
await this.updateAfterMessagesFetched();
/**
......@@ -509,12 +414,8 @@ converse.plugins.add('converse-chatview', {
const all_resources_support_spolers = results.reduce((acc, val) => (acc && val), true);
if (all_resources_support_spolers) {
const html = tpl_spoiler_button(this.model.toJSON());
if (_converse.visible_toolbar_buttons.emoji) {
this.el.querySelector('.toggle-smiley').insertAdjacentHTML('afterEnd', html);
} else {
this.el.querySelector('.chat-toolbar').insertAdjacentHTML('afterBegin', html);
}
}
},
insertHeading () {
......@@ -544,9 +445,7 @@ converse.plugins.add('converse-chatview', {
'message_limit': _converse.message_limit,
'show_call_button': _converse.visible_toolbar_buttons.call,
'show_spoiler_button': _converse.visible_toolbar_buttons.spoiler,
'tooltip_insert_smiley': __('Insert emojis'),
'tooltip_start_call': __('Start a call'),
'use_emoji': _converse.visible_toolbar_buttons.emoji,
}
},
......@@ -987,10 +886,7 @@ converse.plugins.add('converse-chatview', {
} else if (ev.keyCode === _converse.keycodes.ESCAPE) {
return this.onEscapePressed(ev);
} else if (ev.keyCode === _converse.keycodes.ENTER) {
if (this.emoji_dropdown && u.isVisible(this.emoji_dropdown.el.querySelector('.emoji-picker'))) {
this.emoji_dropdown.toggle();
}
return this.onFormSubmitted(ev);
return this.onEnterPressed(ev);
} else if (ev.keyCode === _converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
const textarea = this.el.querySelector('.chat-textarea');
if (!textarea.value || u.hasClass('correcting', textarea)) {
......@@ -1022,6 +918,10 @@ converse.plugins.add('converse-chatview', {
return this.model.messages.filter({'sender': 'me'});
},
onEnterPressed (ev) {
return this.onFormSubmitted(ev);
},
onEscapePressed (ev) {
ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting'),
......@@ -1143,35 +1043,6 @@ converse.plugins.add('converse-chatview', {
u.placeCaretAtEnd(textarea);
},
createEmojiPicker () {
if (_converse.emojipicker === undefined) {
const storage = _converse.config.get('storage'),
id = `converse.emoji-${_converse.bare_jid}`;
_converse.emojipicker = new _converse.EmojiPicker({'id': id});
_converse.emojipicker.browserStorage = new BrowserStorage[storage](id);
_converse.emojipicker.fetch();
}
this.emoji_picker_view = new _converse.EmojiPickerView({'model': _converse.emojipicker});
},
insertEmoji (ev) {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
this.insertIntoTextArea(target.getAttribute('data-emoji'));
},
toggleEmojiMenu (ev) {
if (this.emoji_dropdown === undefined) {
ev.stopPropagation();
const dropdown_el = this.el.querySelector('.toggle-smiley.dropup');
this.emoji_dropdown = new bootstrap.Dropdown(dropdown_el, true);
this.emoji_dropdown.el = dropdown_el;
this.emoji_dropdown.toggle();
}
},
toggleCall (ev) {
ev.stopPropagation();
/**
......@@ -1269,19 +1140,6 @@ converse.plugins.add('converse-chatview', {
return this;
},
async renderEmojiPicker () {
await _converse.api.waitUntil('emojisInitialized');
this.emoji_picker_view.render();
},
insertEmojiPicker () {
const picker_el = this.el.querySelector('.emoji-picker');
if (picker_el !== null) {
picker_el.innerHTML = '';
picker_el.appendChild(this.emoji_picker_view.el);
}
},
emitFocused () {
/**
* Triggered when the focus has been moved to a particular chat.
......
// Converse.js
// https://conversejs.org
//
// Copyright (c) 2013-2019, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
/**
* @module converse-emoji-views
*/
import "@converse/headless/converse-emoji";
import BrowserStorage from "backbone.browserStorage";
import bootstrap from "bootstrap.native";
import tpl_emoji_button from "templates/emoji_button.html";
import tpl_emojis from "templates/emojis.html";
const { Backbone } = converse.env;
const u = converse.env.utils;
converse.plugins.add('converse-emoji-views', {
/* Plugin dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin.
*
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found. By default it's
* false, which means these plugins are only loaded opportunistically.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-emoji", "converse-chatview"],
overrides: {
ChatBoxView: {
events: {
'click .toggle-smiley': 'toggleEmojiMenu',
},
onEnterPressed () {
if (this.emoji_dropdown && u.isVisible(this.emoji_dropdown.el.querySelector('.emoji-picker'))) {
this.emoji_dropdown.toggle();
}
this.__super__.onEnterPressed.apply(this, arguments);
}
},
ChatRoomView: {
events: {
'click .toggle-smiley': 'toggleEmojiMenu'
},
}
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this;
const { __ } = _converse;
_converse.api.settings.update({
'use_system_emojis': true,
'visible_toolbar_buttons': {
'emoji': true
},
});
const emoji_aware_chat_view = {
createEmojiPicker () {
if (_converse.emojipicker === undefined) {
const storage = _converse.config.get('storage'),
id = `converse.emoji-${_converse.bare_jid}`;
_converse.emojipicker = new _converse.EmojiPicker({'id': id});
_converse.emojipicker.browserStorage = new BrowserStorage[storage](id);
_converse.emojipicker.fetch();
}
this.emoji_picker_view = new _converse.EmojiPickerView({'model': _converse.emojipicker});
this.emoji_picker_view.chatview = this;
},
async toggleEmojiMenu (ev) {
if (this.emoji_dropdown === undefined) {
ev.stopPropagation();
const dropdown_el = this.el.querySelector('.toggle-smiley.dropup');
this.emoji_dropdown = new bootstrap.Dropdown(dropdown_el, true);
this.emoji_dropdown.el = dropdown_el;
this.emoji_dropdown.toggle();
await _converse.api.waitUntil('emojisInitialized');
this.emoji_picker_view.render();
this.emoji_picker_view.setScrollPosition();
}
},
insertEmojiPicker () {
const picker_el = this.el.querySelector('.emoji-picker');
if (picker_el !== null) {
picker_el.innerHTML = '';
picker_el.appendChild(this.emoji_picker_view.el);
}
}
};
Object.assign(_converse.ChatBoxView.prototype, emoji_aware_chat_view);
function emojiShouldBeHidden (shortname, current_skintone, toned_emojis) {
// Helper method for the template which decides whether an
// emoji should be hidden, based on which skin tone is
// currently being applied.
if (shortname.includes('_tone')) {
if (!current_skintone || !shortname.includes(current_skintone)) {
return true;
}
} else {
if (current_skintone && toned_emojis.includes(shortname)) {
return true;
}
}
return false;
}
_converse.EmojiPickerView = Backbone.VDOMView.extend({
className: 'emoji-picker__container',
events: {
'click .emoji-category-picker li.emoji-category': 'chooseCategory',
'click .emoji-skintone-picker li.emoji-skintone': 'chooseSkinTone',
'click .toggle-smiley ul.emoji-picker li': 'insertEmoji'
},
initialize () {
this.model.on('change:current_skintone', this.render, this);
this.model.on('change:current_category', () => {
this.render();
this.setScrollPosition();
});
_converse.api.trigger('emojiPickerViewInitialized');
},
toHTML () {
const html = tpl_emojis(
Object.assign(
this.model.toJSON(), {
'_converse': _converse,
'emoji_categories': _converse.emoji_categories,
'emojis_by_category': u.getEmojisByCategory(),
'shouldBeHidden': emojiShouldBeHidden,
'skintones': ['tone1', 'tone2', 'tone3', 'tone4', 'tone5'],
'toned_emojis': _converse.emojis.toned,
'transform': u.getEmojiRenderer()
}
)
);
return html;
},
chooseSkinTone (ev) {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target.nodeName === 'IMG' ?
ev.target.parentElement : ev.target;
const skintone = target.getAttribute("data-skintone").trim();
if (this.model.get('current_skintone') === skintone) {
this.model.save({'current_skintone': ''});
} else {
this.model.save({'current_skintone': skintone});
}
},
chooseCategory (ev) {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
const category = target.getAttribute("data-category").trim();
this.model.save({'current_category': category});
},
setScrollPosition () {
const category = this.model.get('current_category');
const el = this.el.querySelector('.emoji-picker__lists');
const heading = this.el.querySelector(`#emoji-picker-${category}`);
el.scrollTop = heading.offsetTop - heading.offsetHeight*2;
},
insertEmoji (ev) {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
this.chatview.insertIntoTextArea(target.getAttribute('data-emoji'));
}
});
/************************ BEGIN Event Handlers ************************/
_converse.api.listen.on('renderToolbar', view => {
if (_converse.visible_toolbar_buttons.emoji) {
const html = tpl_emoji_button({'tooltip_insert_smiley': __('Insert emojis')});
view.el.querySelector('.chat-toolbar').insertAdjacentHTML('afterBegin', html);
view.createEmojiPicker();
view.insertEmojiPicker();
}
});
}
});
......@@ -624,8 +624,6 @@ converse.plugins.add('converse-muc-views', {
'click .show-room-details-modal': 'showRoomDetailsModal',
'click .toggle-call': 'toggleCall',
'click .toggle-occupants': 'toggleOccupants',
'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
'click .toggle-smiley': 'toggleEmojiMenu',
'click .upload-file': 'toggleFileUpload',
'keydown .chat-textarea': 'onKeyDown',
'keyup .chat-textarea': 'onKeyUp',
......@@ -635,7 +633,7 @@ converse.plugins.add('converse-muc-views', {
'drop .chat-textarea': 'onDrop',
},
async initialize () {
initialize () {
this.initDebounced();
this.model.messages.on('add', this.onMessageAdded, this);
......@@ -662,9 +660,6 @@ converse.plugins.add('converse-muc-views', {
this.model.occupants.on('change:affiliation', this.onOccupantAffiliationChanged, this);
this.render();
this.createEmojiPicker();
this.insertEmojiPicker();
await this.renderEmojiPicker();
this.updateAfterMessagesFetched();
this.createOccupantsView();
this.onConnectionStatusChanged();
......
......@@ -8,6 +8,7 @@ import "converse-bookmark-views"; // Views for XEP-0048 Bookmarks
import "converse-chatview"; // Renders standalone chat boxes for single user chat
import "converse-controlbox"; // The control box
import "converse-dragresize"; // Allows chat boxes to be resized by dragging them
import "converse-emoji-views";
import "converse-fullscreen";
import "converse-headline"; // Support for headline messages
import "converse-mam-views";
......@@ -15,8 +16,8 @@ import "converse-minimize"; // Allows chat boxes to be minimized
import "converse-muc-views"; // Views related to MUC
import "converse-notification"; // HTML5 Notifications
import "converse-omemo";
import "converse-push"; // XEP-0357 Push Notifications
import "converse-profile";
import "converse-push"; // XEP-0357 Push Notifications
import "converse-register"; // XEP-0077 In-band registration
import "converse-roomslist"; // Show currently open chat rooms
import "converse-rosterview";
......@@ -33,6 +34,7 @@ const WHITELISTED_PLUGINS = [
'converse-chatview',
'converse-controlbox',
'converse-dragresize',
'converse-emoji-views',
'converse-fullscreen',
'converse-headline',
'converse-mam-views',
......
......@@ -10,7 +10,7 @@ import * as twemoji from "twemoji";
import _ from "./lodash.noconflict";
import converse from "./converse-core";
const { Strophe } = converse.env;
const { Backbone, Strophe } = converse.env;
const u = converse.env.utils;
const ASCII_LIST = {
......@@ -198,6 +198,14 @@ converse.plugins.add('converse-emoji', {
"flags": __("Flags")
}
_converse.EmojiPicker = Backbone.Model.extend({
defaults: {
'current_category': 'people',
'current_skintone': '',
'scroll_position': 0
}
});
_converse.emojis = {};
u.getEmojiRenderer = function () {
......
<li class="toggle-toolbar-menu toggle-smiley dropup">
<a class="toggle-smiley far fa-smile" title="{{{o.tooltip_insert_smiley}}}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></a>
<div class="emoji-picker dropdown-menu toolbar-menu"></div>
</li>
......@@ -12,7 +12,7 @@
{[ Object.keys(o.emoji_categories).forEach(function (category) { ]}
<a id="emoji-picker-{{{category}}}" class="emoji-category__heading">{{{o._converse.emoji_category_labels[category]}}}</a>
<ul class="emoji-picker emoji-picker-{{{category}}}">
{[ o._.forEach(o.emojis_by_category[category], function (emoji) { ]}
{[ o.emojis_by_category[category].forEach(function (emoji) { ]}
<li class="emoji insert-emoji {[ if (o.shouldBeHidden(emoji.sn, o.current_skintone, o.toned_emojis)) { ]} hidden {[ }; ]}"
data-emoji="{{{emoji.sn}}}" title="{{{emoji.sn}}}">
<a href="#" data-emoji="{{{emoji.sn}}}"> {{ o.transform(emoji.sn) }} </a>
......@@ -21,11 +21,14 @@
</ul>
{[ }); ]}
</div>
<ul class="emoji-skintone-picker">
{[ o._.forEach(o.skintones, function (skintone) { ]}
<div class="emoji-skintone-picker">
<label>Skin tone:</label>
<ul>
{[ o.skintones.forEach(function (skintone) { ]}
<li data-skintone="{{{skintone}}}" class="emoji-skintone {[ if (o.current_skintone === skintone) { ]} picked {[ } ]}">
<a class="pick-skintone" href="#" data-skintone="{{{skintone}}}"> {{ o.transform(':'+skintone+':') }} </a>
</li>
{[ }); ]}
</ul>
</div>
</div>
{[ if (o.use_emoji) { ]}
<li class="toggle-toolbar-menu toggle-smiley dropup">
<a class="toggle-smiley far fa-smile" title="{{{o.tooltip_insert_smiley}}}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></a>
<div class="emoji-picker dropdown-menu toolbar-menu"></div>
</li>
{[ } ]}
{[ if (o.show_call_button) { ]}
<li class="toggle-call fa fa-phone" title="{{{o.label_start_call}}}"></li>
{[ } ]}
......
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