123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828 |
- /**
- * This file is part of Threema Web.
- *
- * Threema Web is free software: you can redistribute it and/or modify it
- * under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or (at
- * your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
- * General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
- */
- import {BrowserService} from '../services/browser';
- import {StringService} from '../services/string';
- /**
- * The compose area where messages are written.
- */
- export default [
- 'BrowserService',
- 'StringService',
- '$window',
- '$timeout',
- '$translate',
- '$mdDialog',
- '$filter',
- '$log',
- '$rootScope',
- function(browserService: BrowserService,
- stringService: StringService,
- $window, $timeout: ng.ITimeoutService,
- $translate: ng.translate.ITranslateService,
- $mdDialog: ng.material.IDialogService,
- $filter: ng.IFilterService,
- $log: ng.ILogService,
- $rootScope: ng.IRootScopeService) {
- return {
- restrict: 'EA',
- scope: {
- // Callback to submit text or file data
- submit: '=',
- // Callbacks to update typing information
- startTyping: '=',
- stopTyping: '=',
- onTyping: '=',
- onKeyDown: '=',
- // Reference to initial text and draft
- initialData: '=',
- // Callback that is called when uploading files
- onUploading: '=',
- maxTextLength: '=',
- },
- link(scope: any, element) {
- // Logging
- const logTag = '[Directives.ComposeArea]';
- // Constants
- const TRIGGER_ENABLED_CSS_CLASS = 'is-enabled';
- const TRIGGER_ACTIVE_CSS_CLASS = 'is-active';
- // Elements
- const composeArea: any = element;
- const composeDiv: any = angular.element(element[0].querySelector('div.compose'));
- const emojiTrigger: any = angular.element(element[0].querySelector('i.emoji-trigger'));
- const emojiKeyboard: any = angular.element(element[0].querySelector('.emoji-keyboard'));
- const sendTrigger: any = angular.element(element[0].querySelector('i.send-trigger'));
- const fileTrigger: any = angular.element(element[0].querySelector('i.file-trigger'));
- const fileInput: any = angular.element(element[0].querySelector('input.file-input'));
- // Set initial text
- if (scope.initialData.initialText) {
- composeDiv[0].innerText = scope.initialData.initialText;
- scope.initialData.initialText = '';
- } else if (scope.initialData.draft !== undefined) {
- composeDiv[0].innerText = scope.initialData.draft;
- }
- let caretPosition: {
- from?: number,
- to?: number,
- fromBytes?: number,
- toBytes?: number } = null;
- /**
- * Stop propagation of click events and hold htmlElement of the emojipicker
- */
- const EmojiPickerContainer = (function() {
- let instance;
- function click(e) {
- e.stopPropagation();
- }
- return {
- get: function() {
- if (instance === undefined) {
- instance = {
- htmlElement: composeArea[0].querySelector('div.emojione-picker'),
- };
- // append stop propagation
- angular.element(instance.htmlElement).on('click', click);
- }
- return instance;
- },
- destroy: function() {
- if (instance !== undefined) {
- // remove stop propagation
- angular.element(instance.htmlElement).off('click', click);
- instance = undefined;
- }
- },
- };
- })();
- // Typing events
- let stopTypingTimer: ng.IPromise<void> = null;
- function stopTyping() {
- // We can only stop typing of the timer is set (meaning
- // that we started typing earlier)
- if (stopTypingTimer !== null) {
- // Cancel timer
- $timeout.cancel(stopTypingTimer);
- stopTypingTimer = null;
- // Send stop typing message
- scope.stopTyping();
- }
- }
- function startTyping() {
- if (stopTypingTimer === null) {
- // If the timer wasn't set previously, we just
- // started typing!
- scope.startTyping();
- } else {
- // Cancel timer, we'll re-create it
- $timeout.cancel(stopTypingTimer);
- }
- // Define a timeout to send the stopTyping event
- stopTypingTimer = $timeout(stopTyping, 10000);
- }
- // Process a DOM node recursively and extract text from compose area.
- function getText(trim = true) {
- let text = '';
- const visitChildNodes = (parentNode: HTMLElement) => {
- // tslint:disable-next-line: prefer-for-of (see #98)
- for (let i = 0; i < parentNode.childNodes.length; i++) {
- const node = parentNode.childNodes[i] as HTMLElement;
- switch (node.nodeType) {
- case Node.TEXT_NODE:
- // Append text, but strip leading and trailing newlines
- text += node.nodeValue.replace(/(^[\r\n]*|[\r\n]*$)/g, '');
- break;
- case Node.ELEMENT_NODE:
- const tag = node.tagName.toLowerCase();
- if (tag === 'div') {
- visitChildNodes(node);
- break;
- } else if (tag === 'img') {
- text += (node as HTMLImageElement).alt;
- break;
- } else if (tag === 'br') {
- text += '\n';
- break;
- } else if (tag === 'span' && node.hasAttribute('text')) {
- text += node.getAttributeNode('text').value;
- break;
- }
- default:
- $log.warn(logTag, 'Unhandled node:', node);
- }
- }
- };
- visitChildNodes(composeDiv[0]);
- return trim ? text.trim() : text;
- }
- // Determine whether field is empty
- function composeAreaIsEmpty() {
- return getText().length === 0;
- }
- // Submit the text from the compose area.
- //
- // Emoji images are converted to their alt text in this process.
- function submitText(): Promise<any> {
- const text = getText();
- return new Promise((resolve, reject) => {
- const submitTexts = (strings: string[]) => {
- const messages: threema.TextMessageData[] = [];
- for (const piece of strings) {
- messages.push({
- text: piece,
- });
- }
- scope.submit('text', messages)
- .then(resolve)
- .catch(reject);
- };
- const fullText = text.trim().replace(/\r/g, '');
- if (fullText.length > scope.maxTextLength) {
- const pieces: string[] = stringService.byteChunk(fullText, scope.maxTextLength, 50);
- const confirm = $mdDialog.confirm()
- .title($translate.instant('messenger.MESSAGE_TOO_LONG_SPLIT_SUBJECT'))
- .textContent($translate.instant('messenger.MESSAGE_TOO_LONG_SPLIT_BODY', {
- max: scope.maxTextLength,
- count: pieces.length,
- }))
- .ok($translate.instant('common.YES'))
- .cancel($translate.instant('common.NO'));
- $mdDialog.show(confirm).then(function() {
- submitTexts(pieces);
- }, () => {
- reject();
- });
- } else {
- submitTexts([fullText]);
- }
- });
- }
- function sendText(): boolean {
- if (!composeAreaIsEmpty()) {
- submitText().then(() => {
- // Clear compose div
- composeDiv[0].innerText = '';
- composeDiv[0].focus();
- // Send stopTyping event
- stopTyping();
- // Clear draft
- scope.onTyping('');
- updateView();
- }).catch(() => {
- // do nothing
- $log.warn(logTag, 'Failed to submit text');
- });
- return true;
- }
- return false;
- }
- // Handle typing events
- function onKeyDown(ev: KeyboardEvent): void {
- // If enter is pressed, prevent default event from being dispatched
- if (!ev.shiftKey && ev.key === 'Enter') {
- ev.preventDefault();
- }
- // If the keydown is handled and aborted outside
- if (scope.onKeyDown && scope.onKeyDown(ev) !== true) {
- ev.preventDefault();
- return;
- }
- // At link time, the element is not yet evaluated.
- // Therefore add following code to end of event loop.
- $timeout(() => {
- // Shift + enter to insert a newline. Enter to send.
- if (!ev.shiftKey && ev.key === 'Enter') {
- if (sendText()) {
- return;
- }
- }
- updateView();
- }, 0);
- }
- function onKeyUp(ev: KeyboardEvent): void {
- // At link time, the element is not yet evaluated.
- // Therefore add following code to end of event loop.
- $timeout(() => {
- // If the compose area contains only a single <br>, make it fully empty.
- // See also: https://stackoverflow.com/q/14638887/284318
- const text = getText(false);
- if (text === '\n') {
- composeDiv[0].innerText = '';
- } else if (ev.keyCode === 190) {
- // A ':' is pressed, try to parse
- const currentWord = stringService.getWord(text, caretPosition.fromBytes, [':']);
- if (currentWord.length > 2
- && currentWord.substr(0, 1) === ':') {
- const unicodeEmoji = emojione.shortnameToUnicode(currentWord);
- if (unicodeEmoji && unicodeEmoji !== currentWord) {
- return insertEmoji(unicodeEmoji,
- caretPosition.from - currentWord.length,
- caretPosition.to);
- }
- }
- }
- // Update typing information (use text instead method)
- if (text.trim().length === 0) {
- stopTyping();
- scope.onTyping('');
- } else {
- startTyping();
- scope.onTyping(text.trim(), stringService.getWord(text, caretPosition.from));
- }
- updateView();
- }, 0);
- }
- // Function to fetch file contents
- // Resolve to ArrayBuffer or reject to ErrorEvent.
- function fetchFileListContents(fileList: FileList): Promise<Map<File, ArrayBuffer>> {
- return new Promise((resolve) => {
- const buffers = new Map<File, ArrayBuffer>();
- const fileCounter = fileList.length;
- const next = (file: File, res: ArrayBuffer | null, error: any) => {
- buffers.set(file, res);
- if (buffers.size >= fileCounter) {
- resolve(buffers);
- }
- };
- for (let n = 0; n < fileCounter; n++) {
- const reader = new FileReader();
- const file = fileList.item(n);
- reader.onload = (ev: Event) => {
- next(file, (ev.target as FileReader).result, ev);
- };
- reader.onerror = (ev: ErrorEvent) => {
- // set a null object
- next(file, null, ev);
- };
- reader.onprogress = function(data) {
- if (data.lengthComputable) {
- const progress = ((data.loaded / data.total) * 100);
- scope.onUploading(true, progress, 100 / fileCounter * n);
- }
- };
- reader.readAsArrayBuffer(file);
- }
- });
- }
- function uploadFiles(fileList: FileList): void {
- scope.onUploading(true, 0, 0);
- fetchFileListContents(fileList).then((data: Map<File, ArrayBuffer>) => {
- const fileMessages = [];
- data.forEach((buffer, file) => {
- const fileMessageData: threema.FileMessageData = {
- name: file.name,
- fileType: file.type,
- size: file.size,
- data: buffer,
- };
- // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1240259
- if (browserService.getBrowser().firefox) {
- if (fileMessageData.name.endsWith('.ogg') && fileMessageData.fileType === 'video/ogg') {
- fileMessageData.fileType = 'audio/ogg';
- }
- }
- fileMessages.push(fileMessageData);
- });
- scope.submit('file', fileMessages);
- scope.onUploading(false);
- }).catch((ev: ErrorEvent) => {
- $log.error(logTag, 'Could not load file:', ev.message);
- });
- }
- // Handle pasting
- function onPaste(ev: ClipboardEvent) {
- ev.preventDefault();
- // If no clipboard data is available, do nothing.
- if (!ev.clipboardData) {
- return;
- }
- // Extract pasted items
- const items: DataTransferItemList = ev.clipboardData.items;
- if (!items) {
- return;
- }
- // Find available types
- let fileIdx: number | null = null;
- let textIdx: number | null = null;
- for (let i = 0; i < items.length; i++) {
- if (items[i].type.indexOf('image/') !== -1 || items[i].type === 'application/x-moz-file') {
- fileIdx = i;
- } else if (items[i].type === 'text/plain') {
- textIdx = i;
- }
- }
- // Handle pasting of files
- if (fileIdx !== null) {
- // Read clipboard data as blob
- const blob: Blob = items[fileIdx].getAsFile();
- // Convert blob to arraybuffer
- const reader = new FileReader();
- reader.onload = function() {
- const buffer: ArrayBuffer = this.result;
- // Construct file name
- let fileName: string;
- if ((blob as any).name) {
- fileName = (blob as any).name;
- } else if (blob.type && blob.type.match(/^[^;]*\//) !== null) {
- const fileExt = blob.type.split(';')[0].split('/')[1];
- fileName = 'clipboard.' + fileExt;
- } else {
- $log.warn(logTag, 'Pasted file has an invalid MIME type: "' + blob.type + '"');
- return;
- }
- // Send data as file
- const fileMessageData: threema.FileMessageData = {
- name: fileName,
- fileType: blob.type,
- size: blob.size,
- data: buffer,
- };
- scope.submit('file', [fileMessageData]);
- };
- reader.readAsArrayBuffer(blob);
- // Handle pasting of text
- } else if (textIdx !== null) {
- const text = ev.clipboardData.getData('text/plain');
- // Look up some filter functions
- const escapeHtml = $filter('escapeHtml') as (a: string) => string;
- const emojify = $filter('emojify') as (a: string, b?: boolean) => string;
- const mentionify = $filter('mentionify') as (a: string) => string;
- const nlToBr = $filter('nlToBr') as (a: string, b?: boolean) => string;
- // Escape HTML markup
- const escaped = escapeHtml(text);
- // Apply filters (emojify, convert newline, etc)
- const formatted = nlToBr(mentionify(emojify(escaped, true)), true);
- // Insert resulting HTML
- document.execCommand('insertHTML', false, formatted);
- cleanupComposeContent();
- updateView();
- }
- }
- // Translate placeholder texts
- let regularPlaceholder = '';
- let dragoverPlaceholder = '';
- $translate('messenger.COMPOSE_MESSAGE').then((translated) => regularPlaceholder = translated);
- $translate('messenger.COMPOSE_MESSAGE_DRAGOVER').then((translated) => dragoverPlaceholder = translated);
- // Show emoji picker element
- function showEmojiPicker() {
- const emojiPicker: HTMLElement = EmojiPickerContainer.get().htmlElement;
- // Show
- emojiKeyboard.addClass('active');
- emojiTrigger.addClass(TRIGGER_ACTIVE_CSS_CLASS);
- // Find all emoji
- const allEmoji: any = angular.element(emojiPicker.querySelectorAll('.content .e1'));
- // Add event handlers
- allEmoji.on('click', onEmojiChosen);
- // set focus to fix chat scroll bug
- $timeout(() => {
- composeDiv[0].focus();
- });
- }
- // Hide emoji picker element
- function hideEmojiPicker() {
- // Hide
- emojiKeyboard.removeClass('active');
- emojiTrigger.removeClass(TRIGGER_ACTIVE_CSS_CLASS);
- // Find all emoji
- const allEmoji: any = angular.element(
- EmojiPickerContainer.get().htmlElement.querySelectorAll('.content .e1'));
- // Remove event handlers
- allEmoji.off('click', onEmojiChosen);
- EmojiPickerContainer.destroy();
- }
- // Emoji trigger is clicked
- function onEmojiTrigger(ev: MouseEvent): void {
- ev.stopPropagation();
- // Toggle visibility of picker
- if (emojiKeyboard.hasClass('active')) {
- hideEmojiPicker();
- } else {
- showEmojiPicker();
- }
- }
- // Emoji is chosen
- function onEmojiChosen(ev: MouseEvent): void {
- ev.stopPropagation();
- insertEmoji (this.textContent);
- }
- function insertEmoji(emoji, posFrom = null, posTo = null): void {
- const emojiElement = ($filter('emojify') as any)(emoji, true, true) as string;
- insertHTMLElement(emoji, emojiElement, posFrom, posTo);
- }
- function insertMention(mentionString, posFrom = null, posTo = null): void {
- const mentionElement = ($filter('mentionify') as any)(mentionString) as string;
- insertHTMLElement(mentionString, mentionElement, posFrom, posTo);
- }
- function insertHTMLElement(original: string, formatted: string, posFrom = null, posTo = null): void {
- // In Chrome in right-to-left mode, our content editable
- // area may contain a DIV element.
- const childNodes = composeDiv[0].childNodes;
- const nestedDiv = childNodes.length === 1
- && childNodes[0].tagName !== undefined
- && childNodes[0].tagName.toLowerCase() === 'div';
- let contentElement;
- if (nestedDiv === true) {
- contentElement = composeDiv[0].childNodes[0];
- } else {
- contentElement = composeDiv[0];
- }
- let currentHTML = '';
- for (let i = 0; i < contentElement.childNodes.length; i++) {
- const node = contentElement.childNodes[i];
- if (node.nodeType === node.TEXT_NODE) {
- currentHTML += node.textContent;
- } else if (node.nodeType === node.ELEMENT_NODE) {
- const tag = node.tagName.toLowerCase();
- if (tag === 'img' || tag === 'span') {
- currentHTML += getOuterHtml(node);
- } else if (tag === 'br') {
- // Firefox inserts a <br> after editing content editable fields.
- // Remove the last <br> to fix this.
- if (i < contentElement.childNodes.length - 1) {
- currentHTML += getOuterHtml(node);
- }
- }
- }
- }
- if (caretPosition !== null) {
- posFrom = null === posFrom ? caretPosition.from : posFrom;
- posTo = null === posTo ? caretPosition.to : posTo;
- currentHTML = currentHTML.substr(0, posFrom)
- + formatted
- + currentHTML.substr(posTo);
- // change caret position
- caretPosition.from += formatted.length;
- caretPosition.fromBytes += original.length;
- posFrom += formatted.length;
- } else {
- // insert at the end of line
- posFrom = currentHTML.length;
- currentHTML += formatted;
- caretPosition = {
- from: currentHTML.length,
- };
- }
- caretPosition.to = caretPosition.from;
- caretPosition.toBytes = caretPosition.fromBytes;
- contentElement.innerHTML = currentHTML;
- cleanupComposeContent();
- setCaretPosition(posFrom);
- // Update the draft text
- scope.onTyping(getText());
- updateView();
- }
- // File trigger is clicked
- function onFileTrigger(ev: MouseEvent): void {
- ev.preventDefault();
- ev.stopPropagation();
- const input = element[0].querySelector('.file-input') as HTMLInputElement;
- input.click();
- }
- function onSendTrigger(ev: MouseEvent): boolean {
- ev.preventDefault();
- ev.stopPropagation();
- return sendText();
- }
- // File(s) are uploaded via input field
- function onFileSelected() {
- uploadFiles(this.files);
- fileInput.val('');
- }
- // Disable content editable and dragging for contained images (emoji)
- function cleanupComposeContent() {
- for (const img of composeDiv[0].getElementsByTagName('img')) {
- img.ondragstart = () => false;
- }
- for (const span of composeDiv[0].getElementsByTagName('span')) {
- span.setAttribute('contenteditable', false);
- }
- if (browserService.getBrowser().firefox) {
- // disable object resizing is the only way to disable resizing of
- // emoji (contenteditable must be true, otherwise the emoji can not
- // be removed with backspace (in FF))
- document.execCommand('enableObjectResizing', false, false);
- }
- }
- // Set all correct styles
- function updateView() {
- if (composeAreaIsEmpty()) {
- sendTrigger.removeClass(TRIGGER_ENABLED_CSS_CLASS);
- } else {
- sendTrigger.addClass(TRIGGER_ENABLED_CSS_CLASS);
- }
- }
- // return the outer html of a node element
- function getOuterHtml(node: Node): string {
- const pseudoElement = document.createElement('pseudo');
- pseudoElement.appendChild(node.cloneNode(true));
- return pseudoElement.innerHTML;
- }
- // return the html code position of the container element
- function getPositions(offset: number, container: Node): {html: number, text: number} {
- let pos = null;
- let textPos = null;
- if (composeDiv[0].contains(container)) {
- let selectedElement;
- if (container === composeDiv[0]) {
- if (offset === 0) {
- return {
- html: 0, text: 0,
- };
- }
- selectedElement = composeDiv[0].childNodes[offset - 1];
- pos = 0;
- textPos = 0;
- } else {
- selectedElement = container.previousSibling;
- pos = offset;
- textPos = offset;
- }
- while (selectedElement !== null) {
- if (selectedElement.nodeType === Node.TEXT_NODE) {
- pos += selectedElement.textContent.length;
- textPos += selectedElement.textContent.length;
- } else {
- pos += getOuterHtml(selectedElement).length;
- textPos += 1;
- }
- selectedElement = selectedElement.previousSibling;
- }
- }
- return {
- html: pos,
- text: textPos,
- };
- }
- // Update the current caret position or selection
- function updateCaretPosition() {
- caretPosition = null;
- if (window.getSelection && composeDiv[0].innerHTML.length > 0) {
- const selection = window.getSelection();
- if (selection.rangeCount) {
- const range = selection.getRangeAt(0);
- const from = getPositions(range.startOffset, range.startContainer);
- if (from !== null && from.html >= 0) {
- const to = getPositions(range.endOffset, range.endContainer);
- caretPosition = {
- from: from.html,
- to: to.html,
- fromBytes: from.text,
- toBytes: to.text,
- };
- }
- }
- }
- }
- // set the correct cart position in the content editable div, position
- // is the position in the html content (not plain text)
- function setCaretPosition(pos: number) {
- const rangeAt = (node: Node, offset?: number) => {
- const range = document.createRange();
- range.collapse(false);
- if (offset !== undefined) {
- range.setStart(node, offset);
- } else {
- range.setStartAfter(node);
- }
- const sel = window.getSelection();
- sel.removeAllRanges();
- sel.addRange(range);
- };
- for (let i = 0; i < composeDiv[0].childNodes.length; i++) {
- const node = composeDiv[0].childNodes[i];
- let size;
- let offset;
- switch (node.nodeType) {
- case Node.TEXT_NODE:
- size = node.textContent.length;
- offset = pos;
- break;
- case Node.ELEMENT_NODE:
- size = getOuterHtml(node).length ;
- break;
- default:
- $log.warn(logTag, 'Unhandled node:', node);
- }
- if (pos < size) {
- // use this node
- rangeAt(node, offset);
- } else if (i === composeDiv[0].childNodes.length - 1) {
- rangeAt(node);
- }
- pos -= size;
- }
- }
- // Handle typing events
- composeDiv.on('keydown', onKeyDown);
- composeDiv.on('keyup', onKeyUp);
- composeDiv.on('keyup mouseup', updateCaretPosition);
- composeDiv.on('selectionchange', updateCaretPosition);
- // Handle paste event
- composeDiv.on('paste', onPaste);
- // Handle click on emoji trigger
- emojiTrigger.on('click', onEmojiTrigger);
- // Handle click on file trigger
- fileTrigger.on('click', onFileTrigger);
- // Handle file uploads
- fileInput.on('change', onFileSelected);
- // Handle click on send trigger
- sendTrigger.on('click', onSendTrigger);
- updateView();
- // Listen to broadcasts
- const unsubscribeListeners = [];
- unsubscribeListeners.push($rootScope.$on('onQuoted', (event: ng.IAngularEvent, args: any) => {
- composeDiv[0].focus();
- }));
- unsubscribeListeners.push($rootScope.$on('onMentionSelected', (event: ng.IAngularEvent, args: any) => {
- if (args.query && args.mention) {
- // Insert resulting HTML
- insertMention(args.mention, caretPosition ? caretPosition.to - args.query.length : null,
- caretPosition ? caretPosition.to : null);
- }
- }));
- // When switching chat, send stopTyping message
- scope.$on('$destroy', () => {
- unsubscribeListeners.forEach((u) => {
- // Unsubscribe
- u();
- });
- stopTyping();
- });
- },
- // tslint:disable:max-line-length
- template: `
- <div>
- <div>
- <i class="md-primary emoji-trigger trigger is-enabled material-icons">tag_faces</i>
- </div>
- <div>
- <div class="compose" contenteditable translate translate-attr-data-placeholder="messenger.COMPOSE_MESSAGE" autofocus></div>
- </div>
- <div>
- <i class="md-primary send-trigger trigger material-icons">send</i>
- <i class="md-primary file-trigger trigger is-enabled material-icons">attach_file</i>
- <input class="file-input" type="file" style="visibility: hidden" multiple>
- </div>
- </div>
- <div class="emoji-keyboard">
- <ng-include src="'partials/emoji-picker.html'" include-replace></ng-include>
- </div>
- `,
- };
- },
- ];
|