diff --git a/app/css/app.css b/app/css/app.css index d4499477..49d1a8de 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -1762,6 +1762,55 @@ div.im_message_body { user-select: text; } +.im_message_reply_wrap { + display: block; + color: inherit; + text-decoration: none; + margin-bottom: 5px; + height: 42px; + overflow: hidden; +} +.im_message_reply_wrap:hover { + text-decoration: none; + color: inherit; + background: rgba(242, 246, 250, 0.5); +} +.im_message_reply { + border: 0 #77b7e4 solid; + border-left-width: 2px; + padding-left: 8px; +} +.im_message_reply_thumb_wrap { + display: block; + float: left; + width: 42px; + height: 42px; + text-align: center; + position: absolute; +} +.im_message_reply_author { + font-weight: bold; + color: #3a6d99; + margin-top: 2px; + margin-bottom: 3px; +} +.im_message_reply_loading { + padding: 12px 0; +} +.im_reply_message_service { + color: #999; +} +.im_message_reply_body { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.im_message_reply_thumbed .im_message_reply_author, +.im_message_reply_thumbed .im_message_reply_body { + margin-left: 52px; +} + + a.im_message_fwd_photo { position: absolute; margin-top: 1px; @@ -2502,21 +2551,6 @@ img.chat_modal_participant_photo { } - -/* Messages edit panel */ -.im_edit_selected_actions { - text-align: center; -} -.im_edit_delete_btn, -.im_edit_forward_btn { - border-radius: 2px; - padding: 7px 17px; - font-weight: normal; - font-size: 12px; - line-height: 18px; - margin: 6px 6px; -} - .im_edit_panel_title { text-align: center; margin: 0; diff --git a/app/css/desktop.css b/app/css/desktop.css index f6369fce..50e0ee57 100644 --- a/app/css/desktop.css +++ b/app/css/desktop.css @@ -677,6 +677,17 @@ a.footer_link.active:active { opacity: 1; } +/* Messages edit panel */ +.im_edit_delete_btn, +.im_edit_forward_btn, +.im_edit_reply_btn { + border-radius: 2px; + padding: 7px 17px; + font-weight: normal; + font-size: 12px; + line-height: 18px; + margin: 6px 0 6px 14px; +} .im_edit_panel_wrap { padding: 0px 0 38px; margin: 0 24px 0 12px; @@ -686,7 +697,6 @@ a.footer_link.active:active { margin: 0 0 47px 3px; border-bottom: 1px solid #EEE; } -.im_edit_flush_link, .im_edit_cancel_link { display: block; padding: 7px 17px; @@ -695,14 +705,15 @@ a.footer_link.active:active { margin: 6px 6px; } .im_edit_cancel_link { - float: left; -} -.im_edit_flush_link { float: right; } .im_edit_selected_actions { + text-align: left; text-transform: uppercase; } +.im_selected_count { + color: #b9cfe3; +} .im_submit { color: #499dd9; @@ -875,6 +886,7 @@ a.footer_link.active:active { } } + .im_message_fwd_author_wrap { margin: 1px 0 4px; display: inline-block; @@ -986,6 +998,7 @@ a.im_panel_peer_photo .peer_initials { .im_send_field_wrap { margin-bottom: 15px; + position: relative; } .composer_rich_textarea, .composer_textarea { @@ -1076,6 +1089,49 @@ a.im_panel_peer_photo .peer_initials { opacity: 1; } +.im_send_reply_wrap { + margin-bottom: 5px; +} +.im_send_reply_form_wrap a.im_panel_own_photo, +.im_send_reply_form_wrap a.im_panel_peer_photo { + margin-top: 47px; +} +.im_send_reply_cancel { + float: right; + display: block; + width: 18px; + height: 18px; + margin-right: 6px; + margin-top: 5px; + -webkit-transform: translate3d(0,0,0); + padding-top: 7px; +} +.im_send_reply_cancel .icon-reply-bar { + display: block; + background: #999; + width: 18px; + height: 2px; + transform-origin: 50% 50%; +} +.im_send_reply_cancel:hover .icon-reply-bar { + background: #44a1e8; +} +.icon-reply-bar:first-child { + -webkit-transform: rotate(-45deg); + -moz-transform: rotate(-45deg); + -ms-transform: rotate(-45deg); + -o-transform: rotate(-45deg); + transform: rotate(-45deg); + transform-origin: 50% 50%; +} +.icon-reply-bar:last-child { + -webkit-transform: translate3d(0,-2px,0) rotate(45deg); + -moz-transform: translate3d(0,-2px,0) rotate(45deg); + -ms-transform: translate3d(0,-2px,0) rotate(45deg); + -o-transform: translate3d(0,-2px,0) rotate(45deg); + transform: translate3d(0,-2px,0) rotate(45deg); +} + /* Peer modals */ .user_modal_window .modal-dialog { max-width: 480px; diff --git a/app/css/mobile.css b/app/css/mobile.css index a444c245..9d1a4473 100644 --- a/app/css/mobile.css +++ b/app/css/mobile.css @@ -608,6 +608,14 @@ img.im_message_video_thumb, margin-top: 0; } + +.im_message_reply_wrap { + margin-top: 2px; +} +.im_message_reply_author { + font-weight: normal; + font-size: 13px; +} .im_message_fwd_header { font-size: 12px; } @@ -624,7 +632,6 @@ img.im_message_video_thumb, } .im_message_date { font-size: 10px; - /*font-size: 12px;*/ padding: 0; } .im_message_out .im_message_meta { @@ -736,8 +743,14 @@ a.im_message_from_photo { .contacts_modal_search_field { font-size: 1.2em; } +.im_edit_selected_actions { + text-align: center; +} .im_edit_delete_btn, .im_edit_forward_btn { + border-radius: 2px; + font-weight: normal; + line-height: 18px; background: none !important; border: 0 !important; width: 50%; diff --git a/app/js/controllers.js b/app/js/controllers.js index 4007e5e1..254269b8 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -349,7 +349,12 @@ angular.module('myApp.controllers', ['myApp.i18n']) $scope.search = {}; $scope.historyFilter = {mediaType: false}; $scope.historyPeer = {}; - $scope.historyState = {selectActions: false, typing: [], missedCount: 0}; + $scope.historyState = { + selectActions: false, + typing: [], + missedCount: 0, + skipped: false + }; $scope.openSettings = function () { $modal.open({ @@ -862,16 +867,17 @@ angular.module('myApp.controllers', ['myApp.i18n']) StatusManager.start(); $scope.peerHistories = []; - $scope.skippedHistory = false; $scope.selectedMsgs = {}; $scope.selectedCount = 0; $scope.historyState.selectActions = false; $scope.historyState.missedCount = 0; + $scope.historyState.skipped = false; $scope.state = {}; $scope.toggleMessage = toggleMessage; $scope.selectedDelete = selectedDelete; $scope.selectedForward = selectedForward; + $scope.selectedReply = selectedReply; $scope.selectedCancel = selectedCancel; $scope.selectedFlush = selectedFlush; @@ -1066,7 +1072,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) } else { minID = 0; } - $scope.skippedHistory = hasLess = minID > 0; + $scope.historyState.skipped = hasLess = minID > 0; if (morePending) { showMoreHistory(); @@ -1123,7 +1129,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) $scope.historyState.missedCount = 0; hasMore = false; - $scope.skippedHistory = hasLess = false; + $scope.historyState.skipped = hasLess = false; maxID = 0; minID = 0; peerHistory = historiesQueuePush(peerID); @@ -1169,7 +1175,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) : 0; maxID = historyResult.history[historyResult.history.length - 1]; - $scope.skippedHistory = hasLess = minID > 0; + $scope.historyState.skipped = hasLess = minID > 0; hasMore = historyResult.count === null || fetchedLength && fetchedLength < historyResult.count; @@ -1179,7 +1185,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) peerHistory.messages = []; angular.forEach(historyResult.history, function (id) { var message = AppMessagesManager.wrapForHistory(id); - if ($scope.skippedHistory) { + if ($scope.historyState.skipped) { delete message.unread; } if (historyResult.unreadOffset) { @@ -1336,6 +1342,17 @@ angular.module('myApp.controllers', ['myApp.i18n']) } } + function selectedReply () { + if ($scope.selectedCount == 1) { + var selectedMessageID; + angular.forEach($scope.selectedMsgs, function (t, messageID) { + selectedMessageID = messageID; + }); + selectedCancel(); + $scope.$broadcast('reply_selected', selectedMessageID); + } + } + function toggleEdit () { if ($scope.historyState.selectActions) { selectedCancel(); @@ -1374,7 +1391,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) } var curPeer = addedMessage.peerID == $scope.curDialog.peerID; if (curPeer) { - if ($scope.historyFilter.mediaType || $scope.skippedHistory) { + if ($scope.historyFilter.mediaType || $scope.historyState.skipped) { if (addedMessage.my) { returnToRecent(); } else { @@ -1488,7 +1505,7 @@ angular.module('myApp.controllers', ['myApp.i18n']) $scope.$on('history_need_more', showMoreHistory); $rootScope.$watch('idle.isIDLE', function (newVal) { - if (!newVal && $scope.curDialog && $scope.curDialog.peerID && !$scope.historyFilter.mediaType && !$scope.skippedHistory) { + if (!newVal && $scope.curDialog && $scope.curDialog.peerID && !$scope.historyFilter.mediaType && !$scope.historyState.skipped) { AppMessagesManager.readHistory($scope.curDialog.inputPeer); } if (!newVal) { @@ -1506,9 +1523,12 @@ angular.module('myApp.controllers', ['myApp.i18n']) $scope.$watch('curDialog.peer', resetDraft); $scope.$on('user_update', angular.noop); + $scope.$on('reply_selected', function (e, messageID) { + replySelect(messageID); + }); $scope.$on('ui_typing', onTyping); - $scope.draftMessage = {text: '', send: sendMessage}; + $scope.draftMessage = {text: '', send: sendMessage, replyClear: replyClear}; $scope.$watch('draftMessage.text', onMessageChange); $scope.$watch('draftMessage.files', onFilesSelected); $scope.$watch('draftMessage.sticker', onStickerSelected); @@ -1529,11 +1549,14 @@ angular.module('myApp.controllers', ['myApp.i18n']) }); var timeout = 0; + var options = { + replyToMsgID: $scope.draftMessage.replyToMessage && $scope.draftMessage.replyToMessage.id + }; do { (function (peerID, curText, curTimeout) { setTimeout(function () { - AppMessagesManager.sendText(peerID, curText); + AppMessagesManager.sendText(peerID, curText, options); }, curTimeout) })($scope.curDialog.peerID, text.substr(0, 4096), timeout); @@ -1552,6 +1575,8 @@ angular.module('myApp.controllers', ['myApp.i18n']) function resetDraft (newPeer) { + replyClear(); + if (newPeer) { Storage.get('draft' + $scope.curDialog.peerID).then(function (draftText) { // console.log('Restore draft', 'draft' + $scope.curDialog.peerID, draftText); @@ -1566,12 +1591,22 @@ angular.module('myApp.controllers', ['myApp.i18n']) } } + function replySelect(messageID) { + $scope.draftMessage.replyToMessage = AppMessagesManager.wrapForHistory(messageID); + $scope.$broadcast('ui_peer_reply'); + } + + function replyClear() { + delete $scope.draftMessage.replyToMessage; + $scope.$broadcast('ui_peer_reply'); + } + function onMessageChange(newVal) { // console.log('ctrl text changed', newVal); // console.trace('ctrl text changed', newVal); if (newVal && newVal.length) { - if (!$scope.historyFilter.mediaType && !$scope.skippedHistory) { + if (!$scope.historyFilter.mediaType && !$scope.historyState.skipped) { AppMessagesManager.readHistory($scope.curDialog.inputPeer); } @@ -1596,11 +1631,15 @@ angular.module('myApp.controllers', ['myApp.i18n']) if (!angular.isArray(newVal) || !newVal.length) { return; } + var options = { + replyToMsgID: $scope.draftMessage.replyToMessage && $scope.draftMessage.replyToMessage.id, + isMedia: $scope.draftMessage.isMedia + }; + + delete $scope.draftMessage.replyToMessage; for (var i = 0; i < newVal.length; i++) { - AppMessagesManager.sendFile($scope.curDialog.peerID, newVal[i], { - isMedia: $scope.draftMessage.isMedia - }); + AppMessagesManager.sendFile($scope.curDialog.peerID, newVal[i], options); $scope.$broadcast('ui_message_send'); } } diff --git a/app/js/directives.js b/app/js/directives.js index 233b519e..3634825f 100755 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -320,6 +320,78 @@ angular.module('myApp.directives', ['myApp.filters']) templateUrl: templateUrl('message_service') }; }) + + .directive('myReplyMessage', function(AppPhotosManager, AppMessagesManager, AppPeersManager, $rootScope) { + + return { + templateUrl: templateUrl('reply_message'), + scope: { + 'replyMessage': '=myReplyMessage' + }, + link: link + }; + + function link ($scope, element, attrs) { + var message = $scope.replyMessage; + if (!message.loading) { + updateMessage($scope, element); + } else { + var messageID = message.id; + var stopWaiting = $scope.$on('messages_downloaded', function (e, msgIDs) { + if (msgIDs.indexOf(messageID) != -1) { + $scope.replyMessage = AppMessagesManager.wrapForHistory(messageID); + updateMessage($scope, element); + stopWaiting(); + } + }); + } + } + + function updateMessage($scope, element) { + var message = $scope.replyMessage; + var thumbWidth = 42; + var thumbHeight = 42; + var thumbPhotoSize; + if (message.media) { + switch (message.media._) { + case 'messageMediaPhoto': + thumbPhotoSize = AppPhotosManager.choosePhotoSize(message.media.photo, thumbWidth, thumbHeight); + break; + + case 'messageMediaDocument': + thumbPhotoSize = message.media.document.thumb; + break; + + case 'messageMediaVideo': + thumbPhotoSize = message.media.video.thumb; + break; + } + } + + if (thumbPhotoSize && thumbPhotoSize._ != 'photoSizeEmpty') { + var dim = calcImageInBox(thumbPhotoSize.w, thumbPhotoSize.h, thumbWidth, thumbHeight, true); + + $scope.thumb = { + width: dim.w, + height: dim.h, + location: thumbPhotoSize.location, + size: thumbPhotoSize.size + }; + } + + if (element[0].tagName == 'A') { + element.on('click', function () { + var peerID = AppMessagesManager.getMessagePeer(message); + var peerString = AppPeersManager.getPeerString(peerID); + + $rootScope.$broadcast('history_focus', {peerString: peerString, messageID: message.id}); + + }) + } + } + + }) + .directive('myMessagePhoto', function() { return { templateUrl: templateUrl('message_attach_photo') @@ -1212,6 +1284,14 @@ angular.module('myApp.directives', ['myApp.filters']) composer.focus(); } }); + $scope.$on('ui_peer_reply', function () { + onContentLoaded(function () { + $scope.$emit('ui_editor_resize'); + if (!Config.Navigator.touch) { + composer.focus(); + } + }) + }); var sendAwaiting = false; $scope.$on('ui_message_before_send', function () { @@ -2056,8 +2136,6 @@ angular.module('myApp.directives', ['myApp.filters']) onContentLoaded(updateMargin); }); - - }; }) diff --git a/app/js/lib/utils.js b/app/js/lib/utils.js index 6d2bd8b0..b39aa31c 100644 --- a/app/js/lib/utils.js +++ b/app/js/lib/utils.js @@ -280,6 +280,7 @@ function templateUrl (tplName) { error_modal: 'desktop', media_modal_layout: 'desktop', slider: 'desktop', + reply_message: 'desktop' }; var layout = forceLayout[tplName] || (Config.Mobile ? 'mobile' : 'desktop'); return 'partials/' + layout + '/' + tplName + '.html'; diff --git a/app/js/locales/en-us.json b/app/js/locales/en-us.json index 6ce39497..1ac111fd 100644 --- a/app/js/locales/en-us.json +++ b/app/js/locales/en-us.json @@ -325,6 +325,8 @@ "im_clear_history": "Clear History", "im_delete": "Delete {count}", "im_forward": "Forward {count}", + "im_reply": "Reply", + "im_reply_loading": "Loading{dots}", "im_photos_drop_text": "Drop photos here to send", "im_message_field_placeholder": "Write a message...", "im_media_attach_title": "Send media", diff --git a/app/js/services.js b/app/js/services.js index c1fb1e75..94eb905d 100755 --- a/app/js/services.js +++ b/app/js/services.js @@ -764,6 +764,9 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) var lastSearchFilter = {}, lastSearchResults = []; + var needSingleMessages = [], + fetchSingleMessagesTimeout = false; + var serverTimeOffset = 0, timestampNow = tsNow(true), midnightNoOffset = timestampNow - (timestampNow % 86400), @@ -1345,15 +1348,20 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) }); } - function sendText(peerID, text) { + function sendText(peerID, text, options) { + + console.log(peerID, text, options); if (!angular.isString(text) || !text.length) { return; } + options = options || {}; var messageID = tempID--, randomID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)], randomIDS = bigint(randomID[0]).shiftLeft(32).add(bigint(randomID[1])).toString(), historyStorage = historiesStorage[peerID], inputPeer = AppPeersManager.getInputPeerByID(peerID), + flags = 0, + replyToMsgID = options.replyToMsgID, message; if (historyStorage === undefined) { @@ -1361,15 +1369,22 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) } MtpApiManager.getUserID().then(function (fromID) { + if (peerID != fromID) { + flags |= 3; + } + if (replyToMsgID) { + flags |= 8; + } message = { _: 'message', id: messageID, from_id: fromID, to_id: AppPeersManager.getOutputPeer(peerID), - flags: peerID == fromID ? 0 : 3, + flags: flags, date: tsNow(true) + serverTimeOffset, message: text, random_id: randomIDS, + reply_to_msg_id: replyToMsgID, pending: true }; @@ -1399,7 +1414,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) peer: inputPeer, message: text, random_id: randomID, - reply_to_msg_id: 0 + reply_to_msg_id: replyToMsgID }, sentRequestOptions).then(function (sentMessage) { message.date = sentMessage.date; message.id = sentMessage.id; @@ -1450,6 +1465,8 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) randomIDS = bigint(randomID[0]).shiftLeft(32).add(bigint(randomID[1])).toString(), historyStorage = historiesStorage[peerID], inputPeer = AppPeersManager.getInputPeerByID(peerID), + flags = 0, + replyToMsgID = options.replyToMsgID, attachType, apiFileName, realFileName; if (!options.isMedia) { @@ -1474,6 +1491,12 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) } MtpApiManager.getUserID().then(function (fromID) { + if (peerID != fromID) { + flags |= 3; + } + if (replyToMsgID) { + flags |= 8; + } var media = { _: 'messageMediaPending', type: attachType, @@ -1487,11 +1510,12 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) id: messageID, from_id: fromID, to_id: AppPeersManager.getOutputPeer(peerID), - flags: peerID == fromID ? 0 : 3, + flags: flags, date: tsNow(true) + serverTimeOffset, message: '', media: media, random_id: randomIDS, + reply_to_msg_id: replyToMsgID, pending: true }; @@ -1545,7 +1569,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) peer: inputPeer, media: inputMedia, random_id: randomID, - reply_to_msg_id: 0 + reply_to_msg_id: replyToMsgID }).then(function (statedMessage) { message.date = statedMessage.message.date; message.id = statedMessage.message.id; @@ -1940,6 +1964,21 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) } } + var replyToMsgID = message.reply_to_msg_id; + if (replyToMsgID) { + if (messagesStorage[replyToMsgID]) { + message.reply_to_msg = wrapForHistory(replyToMsgID); + } else { + message.reply_to_msg = {id: replyToMsgID, loading: true}; + if (needSingleMessages.indexOf(replyToMsgID) == -1) { + needSingleMessages.push(replyToMsgID); + if (fetchSingleMessagesTimeout === false) { + fetchSingleMessagesTimeout = setTimeout(fetchSingleMessages, 100); + } + } + } + } + if (message.message && message.message.length) { var options = {}; if (!Config.Navigator.mobile) { @@ -1954,6 +1993,27 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) return messagesForHistory[msgID] = message; } + function fetchSingleMessages () { + if (fetchSingleMessagesTimeout !== false) { + clearTimeout(fetchSingleMessagesTimeout); + fetchSingleMessagesTimeout = false; + } + if (!needSingleMessages.length) { + return; + } + var msgIDs = needSingleMessages.slice(); + needSingleMessages = []; + MtpApiManager.invokeApi('messages.getMessages', { + id: msgIDs + }).then(function (getMessagesResult) { + AppUsersManager.saveApiUsers(getMessagesResult.users); + AppChatsManager.saveApiChats(getMessagesResult.chats); + saveMessages(getMessagesResult.messages); + + $rootScope.$broadcast('messages_downloaded', msgIDs); + }) + } + function regroupWrappedHistory (history, limit) { if (!history || !history.length) { return false; @@ -2006,7 +2066,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) !curMessage.action && curMessage.date < prevMessage.date + 900) { - var singleLine = curMessage.message && curMessage.message.length < 70 && curMessage.message.indexOf("\n") == -1; + var singleLine = curMessage.message && curMessage.message.length < 70 && curMessage.message.indexOf("\n") == -1 && !curMessage.reply_to_msg_id; if (groupFwd && curMessage.fwd_from_id && curMessage.fwd_from_id == prevMessage.fwd_from_id) { curMessage.grouped = singleLine ? 'im_grouped_fwd_short' : 'im_grouped_fwd'; } else { diff --git a/app/partials/desktop/head.html b/app/partials/desktop/head.html index b3b0e563..a2504bb4 100644 --- a/app/partials/desktop/head.html +++ b/app/partials/desktop/head.html @@ -52,7 +52,7 @@ - + diff --git a/app/partials/desktop/im.html b/app/partials/desktop/im.html index 776ffe7d..f65c5831 100644 --- a/app/partials/desktop/im.html +++ b/app/partials/desktop/im.html @@ -142,14 +142,11 @@
-
- - - - +
- + +
@@ -158,7 +155,7 @@
-
+
@@ -167,6 +164,11 @@
+
+ +
+
+
diff --git a/app/partials/desktop/message.html b/app/partials/desktop/message.html index 320ab067..41b0291b 100644 --- a/app/partials/desktop/message.html +++ b/app/partials/desktop/message.html @@ -41,6 +41,8 @@ + +
diff --git a/app/partials/desktop/reply_message.html b/app/partials/desktop/reply_message.html new file mode 100644 index 00000000..5a29f0fe --- /dev/null +++ b/app/partials/desktop/reply_message.html @@ -0,0 +1,55 @@ +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + () + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/app/partials/mobile/message.html b/app/partials/mobile/message.html index 6c8b0c63..21b8c892 100644 --- a/app/partials/mobile/message.html +++ b/app/partials/mobile/message.html @@ -33,6 +33,8 @@ + +