diff --git a/app/css/app.css b/app/css/app.css index 49d1a8de..f3fbae08 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -2190,7 +2190,7 @@ a.composer_emoji_btn:hover { color: #52719a; } .composer_dropdown li a:hover, -.composer_dropdown li a.composer_emoji_option_active { +.composer_dropdown li a.composer_autocomplete_option_active { color: #52719a; background: #f2f6fa; } @@ -2200,6 +2200,19 @@ a.composer_emoji_btn:hover { margin-left: 15px; line-height: 20px; } + +.composer_mention_option { + line-height: 20px; +} +.composer_user_mention { + color: #808080; + margin-left: 10px; +} +.composer_dropdown li a:hover .composer_user_mention, +.composer_dropdown li a.composer_autocomplete_option_active .composer_user_mention { + color: #698192; +} + .composer_sticker_btn { width: 67px; height: 67px; diff --git a/app/js/controllers.js b/app/js/controllers.js index ea93fe2b..2d79109d 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -1519,7 +1519,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) $scope.$on('user_update', angular.noop); }) - .controller('AppImSendController', function ($scope, $timeout, MtpApiManager, Storage, AppPeersManager, AppDocsManager, AppMessagesManager, ApiUpdatesManager, MtpApiFileManager) { + .controller('AppImSendController', function ($scope, $timeout, MtpApiManager, Storage, AppChatsManager, AppUsersManager, AppPeersManager, AppDocsManager, AppMessagesManager, ApiUpdatesManager, MtpApiFileManager) { $scope.$watch('curDialog.peer', resetDraft); $scope.$on('user_update', angular.noop); @@ -1529,6 +1529,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) $scope.$on('ui_typing', onTyping); $scope.draftMessage = {text: '', send: sendMessage, replyClear: replyClear}; + $scope.mentions = {}; $scope.$watch('draftMessage.text', onMessageChange); $scope.$watch('draftMessage.files', onFilesSelected); $scope.$watch('draftMessage.sticker', onStickerSelected); @@ -1573,8 +1574,38 @@ angular.module('myApp.controllers', ['myApp.i18n']) return cancelEvent(e); } + function updateMentions () { + var peerID = $scope.curDialog.peerID; + + if (!peerID || peerID > 0) { + safeReplaceObject($scope.mentions, {}); + $scope.$broadcast('mentions_update'); + return; + } + AppChatsManager.getChatFull(-peerID).then(function (chatFull) { + var participantsVector = (chatFull.participants || {}).participants || []; + + var mentionUsers = []; + var mentionIndex = SearchIndexManager.createIndex(); + + angular.forEach(participantsVector, function (participant) { + var user = AppUsersManager.getUser(participant.user_id); + if (user.username) { + mentionUsers.push(user); + SearchIndexManager.indexObject(user.id, AppUsersManager.getUserSearchText(user.id), mentionIndex); + } + }); + + safeReplaceObject($scope.mentions, { + users: mentionUsers, + index: mentionIndex + }); + $scope.$broadcast('mentions_update'); + }); + } function resetDraft (newPeer) { + updateMentions(); replyClear(); if (newPeer) { @@ -1651,7 +1682,6 @@ angular.module('myApp.controllers', ['myApp.i18n']) var doc = AppDocsManager.getDoc(newVal); if (doc.id && doc.access_hash) { - console.log('sticker', doc); var inputMedia = { _: 'inputMediaDocument', id: { diff --git a/app/js/directives.js b/app/js/directives.js index 92794c5f..85a9b221 100755 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -1150,7 +1150,8 @@ angular.module('myApp.directives', ['myApp.filters']) return { link: link, scope: { - draftMessage: '=' + draftMessage: '=', + mentions: '=' } }; @@ -1201,6 +1202,7 @@ angular.module('myApp.directives', ['myApp.filters']) getSendOnEnter: function () { return sendOnEnter; }, + mentions: $scope.mentions, onMessageSubmit: onMessageSubmit, onFilePaste: onFilePaste }); @@ -1300,6 +1302,10 @@ angular.module('myApp.directives', ['myApp.filters']) }) }); + $scope.$on('mentions_update', function () { + composer.onMentionsUpdated(); + }); + var sendAwaiting = false; $scope.$on('ui_message_before_send', function () { sendAwaiting = true; diff --git a/app/js/message_composer.js b/app/js/message_composer.js index c9ceadce..bece34b7 100644 --- a/app/js/message_composer.js +++ b/app/js/message_composer.js @@ -234,7 +234,7 @@ EmojiTooltip.prototype.createTooltip = function () { this.contentEl.on('mousedown', function (e) { e = e.originalEvent || e; var target = $(e.target), code, sticker; - if (target.hasClass('emoji') || target.hasClass('composer_sticker_image')) { + if (target[0].tagName != 'A') { target = $(target[0].parentNode); } if (code = target.attr('data-code')) { @@ -382,7 +382,7 @@ function EmojiPanel (containerEl, options) { this.containerEl.on('mousedown', function (e) { e = e.originalEvent || e; var target = $(e.target), code; - if (target.hasClass('emoji')) { + if (target[0].tagName != 'A') { target = $(target[0].parentNode); } if (code = target.attr('data-code')) { @@ -436,8 +436,8 @@ function MessageComposer (textarea, options) { var self = this; this.autoCompleteEl.on('mousedown', function (e) { e = e.originalEvent || e; - var target = $(e.target), code; - if (target.hasClass('emoji') || target.hasClass('composer_emoji_shortcut')) { + var target = $(e.target), mention, code; + if (target[0].tagName != 'A') { target = $(target[0].parentNode); } if (code = target.attr('data-code')) { @@ -446,6 +446,11 @@ function MessageComposer (textarea, options) { } EmojiHelper.pushPopularEmoji(code); } + if (mention = target.attr('data-mention')) { + if (self.onMentionSelected) { + self.onMentionSelected(mention); + } + } return cancelEvent(e); }); @@ -455,6 +460,7 @@ function MessageComposer (textarea, options) { this.onMessageSubmit = options.onMessageSubmit; this.getSendOnEnter = options.getSendOnEnter; this.onFilePaste = options.onFilePaste; + this.mentions = options.mentions; } MessageComposer.prototype.setUpInput = function () { @@ -463,6 +469,7 @@ MessageComposer.prototype.setUpInput = function () { } else { this.setUpPlaintext(); } + this.autoCompleteRegEx = /(?:\s|^)(:|@)([A-Za-z0-9\-\+\*_]*)$/; } MessageComposer.prototype.setUpRich = function () { @@ -530,34 +537,39 @@ MessageComposer.prototype.onKeyEvent = function (e) { if (this.autocompleteShown) { if (e.keyCode == 38 || e.keyCode == 40) { // UP / DOWN var next = e.keyCode == 40; - var currentSelected = $(this.autoCompleteEl).find('.composer_emoji_option_active'); + var currentSelected = $(this.autoCompleteEl).find('.composer_autocomplete_option_active'); if (currentSelected.length) { var currentSelectedWrap = currentSelected[0].parentNode; var nextWrap = currentSelectedWrap[next ? 'nextSibling' : 'previousSibling']; - currentSelected.removeClass('composer_emoji_option_active'); + currentSelected.removeClass('composer_autocomplete_option_active'); if (nextWrap) { - $(nextWrap).find('a').addClass('composer_emoji_option_active'); + $(nextWrap).find('a').addClass('composer_autocomplete_option_active'); return cancelEvent(e); } } var childNodes = this.autoCompleteEl[0].childNodes; var nextWrap = childNodes[next ? 0 : childNodes.length - 1]; - $(nextWrap).find('a').addClass('composer_emoji_option_active'); + $(nextWrap).find('a').addClass('composer_autocomplete_option_active'); return cancelEvent(e); } if (e.keyCode == 13) { // ENTER - var currentSelected = $(this.autoCompleteEl).find('.composer_emoji_option_active')/* || - $(this.autoCompleteEl).childNodes[0].find('a')*/; - var code = currentSelected.attr('data-code'); - if (code) { + var currentSelected = $(this.autoCompleteEl).find('.composer_autocomplete_option_active'); + var code, mention; + if (code = currentSelected.attr('data-code')) { this.onEmojiSelected(code, true); EmojiHelper.pushPopularEmoji(code); return cancelEvent(e); } + if (mention = currentSelected.attr('data-mention')) { + if (this.onMentionSelected) { + this.onMentionSelected(mention); + return cancelEvent(e); + } + } checkSubmit = true; } } @@ -638,38 +650,65 @@ MessageComposer.prototype.checkAutocomplete = function () { value = value.substr(0, pos); - var matches = value.match(/(?:\s|^):([A-Za-z0-9\-\+\*_]*)$/); + var matches = value.match(this.autoCompleteRegEx); if (matches) { if (this.previousQuery == matches[0]) { return; } this.previousQuery = matches[0]; - var query = SearchIndexManager.cleanSearchText(matches[1]); - EmojiHelper.getPopularEmoji((function (popular) { - if (query.length) { - var found = EmojiHelper.searchEmojis(query); - if (found.length) { - var popularFound = [], - code, pos; - for (var i = 0, len = popular.length; i < len; i++) { - code = popular[i].code; - pos = found.indexOf(code); - if (pos >= 0) { - popularFound.push(code); - found.splice(pos, 1); - if (!found.length) { - break; - } + var query = SearchIndexManager.cleanSearchText(matches[2]); + + if (matches[1] == '@') { // mentions + if (this.mentions && this.mentions.index) { + if (query.length) { + var foundObject = SearchIndexManager.search(query, this.mentions.index); + var foundUsers = []; + var user; + for (var i = 0, length = this.mentions.users.length; i < length; i++) { + user = this.mentions.users[i]; + if (foundObject[user.id]) { + foundUsers.push(user); } } - this.showEmojiSuggestions(popularFound.concat(found)); + } else { + var foundUsers = this.mentions.users; + } + if (foundUsers.length) { + this.showMentionSuggestions(foundUsers); } else { this.hideSuggestions(); } } else { - this.showEmojiSuggestions(popular); + this.hideSuggestions(); } - }).bind(this)); + } + else { // emoji + EmojiHelper.getPopularEmoji((function (popular) { + if (query.length) { + var found = EmojiHelper.searchEmojis(query); + if (found.length) { + var popularFound = [], + code, pos; + for (var i = 0, len = popular.length; i < len; i++) { + code = popular[i].code; + pos = found.indexOf(code); + if (pos >= 0) { + popularFound.push(code); + found.splice(pos, 1); + if (!found.length) { + break; + } + } + } + this.showEmojiSuggestions(popularFound.concat(found)); + } else { + this.hideSuggestions(); + } + } else { + this.showEmojiSuggestions(popular); + } + }).bind(this)); + } } else { delete this.previousQuery; @@ -819,6 +858,65 @@ MessageComposer.prototype.onEmojiSelected = function (code, autocomplete) { this.onChange(); } +MessageComposer.prototype.onMentionsUpdated = function (username) { + delete this.previousQuery; + if (this.isActive) { + this.checkAutocomplete(); + } +} + +MessageComposer.prototype.onMentionSelected = function (username) { + if (this.richTextareaEl) { + var textarea = this.richTextareaEl[0]; + if (!this.isActive) { + if (!this.restoreSelection()) { + setRichFocus(textarea); + } + } + var valueCaret = getRichValueWithCaret(textarea); + var fullValue = valueCaret[0]; + var pos = valueCaret[1] >= 0 ? valueCaret[1] : fullValue.length; + var suffix = fullValue.substr(pos); + var prefix = fullValue.substr(0, pos); + var matches = prefix.match(/@([A-Za-z0-9\-\+\*_]*)$/); + + var newValuePrefix; + if (matches && matches[0]) { + newValuePrefix = prefix.substr(0, matches.index) + '@' + username; + } else { + newValuePrefix = prefix + '@' + username; + } + textarea.value = newValue; + + this.selId = (this.selId || 0) + 1; + var html = this.getRichHtml(newValuePrefix) + ' ' + this.getRichHtml(suffix); + + this.richTextareaEl.html(html); + setRichFocus(textarea, $('#composer_sel' + this.selId)[0]); + } + else { + var textarea = this.textareaEl[0]; + var fullValue = textarea.value; + var pos = this.isActive ? getFieldSelection(textarea) : fullValue.length; + var suffix = fullValue.substr(pos); + var prefix = fullValue.substr(0, pos); + var matches = prefix.match(/@([A-Za-z0-9\-\+\*_]*)$/); + + if (matches && matches[0]) { + var newValue = prefix.substr(0, matches.index) + '@' + username + ' ' + suffix; + var newPos = matches.index + username.length + 2; + } else { + var newValue = prefix + ':' + username + ': ' + suffix; + var newPos = prefix.length + username.length + 2; + } + textarea.value = newValue; + setFieldSelection(textarea, newPos); + } + + this.hideSuggestions(); + this.onChange(); +} + MessageComposer.prototype.onChange = function (e) { if (this.richTextareaEl) { delete this.keyupStarted; @@ -900,6 +998,23 @@ MessageComposer.prototype.showEmojiSuggestions = function (codes) { this.autocompleteShown = true; } +MessageComposer.prototype.showMentionSuggestions = function (users) { + var html = []; + var user; + var count = Math.min(5, users.length); + var i; + + for (i = 0; i < count; i++) { + user = users[i]; + html.push('
  • ' + user.rFullName + '@' + user.username + '
  • '); + } + + this.autoCompleteEl.html(html.join('')); + this.autoCompleteEl.show(); + this.updatePosition(); + this.autocompleteShown = true; +} + MessageComposer.prototype.updatePosition = function () { var offset = (this.richTextareaEl || this.textareaEl).offset(); var height = this.autoCompleteEl.outerHeight(); diff --git a/app/js/services.js b/app/js/services.js index cf3ce261..3f3755bd 100755 --- a/app/js/services.js +++ b/app/js/services.js @@ -111,7 +111,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) if (apiUser.first_name) { apiUser.rFirstName = RichTextProcessor.wrapRichText(apiUser.first_name, {noLinks: true, noLinebreaks: true}); - apiUser.rFullName = RichTextProcessor.wrapRichText(apiUser.first_name + ' ' + (apiUser.last_name || ''), {noLinks: true, noLinebreaks: true}); + apiUser.rFullName = apiUser.last_name ? RichTextProcessor.wrapRichText(apiUser.first_name + ' ' + (apiUser.last_name || ''), {noLinks: true, noLinebreaks: true}) : apiUser.rFirstName; } else { apiUser.rFirstName = RichTextProcessor.wrapRichText(apiUser.last_name, {noLinks: true, noLinebreaks: true}) || apiUser.rPhone || _('user_first_name_deleted'); apiUser.rFullName = RichTextProcessor.wrapRichText(apiUser.last_name, {noLinks: true, noLinebreaks: true}) || apiUser.rPhone || _('user_name_deleted'); @@ -926,8 +926,9 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) dialog.top_message > maxSeenID ) { var message = getMessage(dialog.top_message); + var notifyPeer = message.flags & 16 ? message.from_id : peerID; if (message.unread && !message.out) { - NotificationsManager.getPeerMuted(peerID).then(function (muted) { + NotificationsManager.getPeerMuted(notifyPeer).then(function (muted) { if (!muted) { Storage.get('notify_nopreview').then(function (no_preview) { notifyAboutMessage(message, no_preview); @@ -1233,7 +1234,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) pts_count: affectedMessages.pts_count } }); - return deletedMessageIDs; + return messageIDs; }); } @@ -1386,8 +1387,6 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) } function sendText(peerID, text, options) { - - console.log(peerID, text, options); if (!angular.isString(text) || !text.length) { return; } diff --git a/app/partials/desktop/im.html b/app/partials/desktop/im.html index f65c5831..2f172a99 100644 --- a/app/partials/desktop/im.html +++ b/app/partials/desktop/im.html @@ -162,7 +162,7 @@ -
    +