diff --git a/app/css/app.css b/app/css/app.css index 67f66afc..c987b565 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -339,13 +339,13 @@ input[type="number"]::-webkit-inner-spin-button { height: 100%; } .modal_close { - background: url(../img/icons/CloseHover_2x.png) 0 0 no-repeat; - background-size: 33px 33px; + background: url(../img/icons/PhotoControls.png) 0 -53px no-repeat; + background-size: 33px 86px; width: 33px; height: 33px; float: right; margin: 60px 30px 0 0; - opacity: 0.5; + opacity: 0.6; pointer-events: none; -webkit-transition : .2s; @@ -354,7 +354,40 @@ input[type="number"]::-webkit-inner-spin-button { transition : .2s; } .modal_close_wrap:hover .modal_close { - opacity: 1; + opacity: 0.85; +} + + +.modal_prev_wrap { + cursor: pointer; + position: fixed; + top: 0; + left: 0; + width: 50%; + height: 100%; +} +.modal_prev { + background: url(../img/icons/PhotoControls.png) 0 0 no-repeat; + background-size: 33px 86px; + width: 33px; + height: 33px; + float: left; + margin: 60px 0 0 30px; + opacity: 0.6; + pointer-events: none; + + -webkit-transition : .2s; + -moz-transition : .2s; + -o-transition : .2s; + transition : .2s; +} +.modal_prev_wrap:hover .modal_prev { + opacity: 0.85; +} + +.is_1x .modal_close, +.is_1x .modal_prev { + background-image: url(../img/icons/PhotoControls_1x.png); } .text-invisible { @@ -1849,6 +1882,12 @@ img.img_fullsize { color: #777; margin: 20px 0 0; } +.media_modal_actions { + margin-top: 20px; +} +.media_modal_action_link { + margin-left: 15px; +} .media_modal_author { font-weight: bold; } diff --git a/app/img/icons/CloseHover_1x.png b/app/img/icons/CloseHover_1x.png deleted file mode 100755 index 3f319431..00000000 Binary files a/app/img/icons/CloseHover_1x.png and /dev/null differ diff --git a/app/img/icons/CloseHover_2x.png b/app/img/icons/CloseHover_2x.png deleted file mode 100755 index ca9c4231..00000000 Binary files a/app/img/icons/CloseHover_2x.png and /dev/null differ diff --git a/app/img/icons/Close_1x.png b/app/img/icons/Close_1x.png deleted file mode 100755 index 3e6f6835..00000000 Binary files a/app/img/icons/Close_1x.png and /dev/null differ diff --git a/app/img/icons/Close_2x.png b/app/img/icons/Close_2x.png deleted file mode 100755 index 1a892be1..00000000 Binary files a/app/img/icons/Close_2x.png and /dev/null differ diff --git a/app/img/icons/PhotoControls.png b/app/img/icons/PhotoControls.png new file mode 100644 index 00000000..8abd1c58 Binary files /dev/null and b/app/img/icons/PhotoControls.png differ diff --git a/app/img/icons/PhotoControls_1x.png b/app/img/icons/PhotoControls_1x.png new file mode 100644 index 00000000..3fdea5f5 Binary files /dev/null and b/app/img/icons/PhotoControls_1x.png differ diff --git a/app/js/controllers.js b/app/js/controllers.js index a7063577..1ae73404 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -866,8 +866,128 @@ angular.module('myApp.controllers', []) } }) - .controller('PhotoModalController', function ($scope, AppPhotosManager) { + .controller('PhotoModalController', function ($q, $scope, $rootScope, $modalInstance, AppPhotosManager, AppMessagesManager, AppPeersManager, PeersSelectService, ErrorService) { + $scope.photo = AppPhotosManager.wrapForFull($scope.photoID); + $scope.nav = {}; + + var peerID = AppMessagesManager.getMessagePeer(AppMessagesManager.getMessage($scope.messageID)), + inputPeer = AppPeersManager.getInputPeerByID(peerID), + inputQuery = '', + inputFilter = {_: 'inputMessagesFilterPhotos'}, + list = [$scope.messageID], + maxID = $scope.messageID, + hasMore = true; + + updatePrevNext(); + + AppMessagesManager.getSearch(inputPeer, inputQuery, inputFilter, 0, 1000).then(function (searchCachedResult) { + // console.log(dT(), 'search cache', searchCachedResult); + if (searchCachedResult.history.indexOf($scope.messageID) >= 0) { + list = searchCachedResult.history; + maxID = list[list.length - 1]; + + updatePrevNext(); + } + // console.log(dT(), list, maxID); + }); + + + var jump = 0; + function movePosition (sign) { + var curIndex = list.indexOf($scope.messageID), + index = curIndex >= 0 ? curIndex + sign : 0, + curJump = ++jump; + + var promise = index >= list.length ? loadMore() : $q.when(); + promise.then(function () { + if (curJump != jump) { + return; + } + + $scope.messageID = list[index]; + $scope.photoID = AppMessagesManager.getMessage($scope.messageID).media.photo.id; + $scope.photo = AppPhotosManager.wrapForFull($scope.photoID); + + updatePrevNext(); + }); + }; + + var loadingPromise = false; + function loadMore () { + if (loadingPromise) return loadingPromise; + + return loadingPromise = AppMessagesManager.getSearch(inputPeer, inputQuery, inputFilter, maxID).then(function (searchResult) { + maxID = searchResult.history[searchResult.history.length - 1]; + list = list.concat(searchResult.history); + + hasMore = searchResult.history.length || list.length < searchResult.count; + updatePrevNext(); + loadingPromise = false; + }); + }; + + function updatePrevNext () { + var index = list.indexOf($scope.messageID); + $scope.nav.hasNext = hasMore || index < list.length - 1; + $scope.nav.hasPrev = index > 0; + }; + + $scope.nav.next = function () { + if (!$scope.nav.hasNext) { + return false; + } + + movePosition(+1); + }; + + $scope.nav.prev = function () { + if (!$scope.nav.hasPrev) { + return false; + } + movePosition(-1); + }; + + $scope.forward = function () { + var messageID = $scope.messageID; + PeersSelectService.selectPeer().then(function (peerString) { + var peerID = AppPeersManager.getPeerID(peerString); + AppMessagesManager.forwardMessages(peerID, [messageID]).then(function () { + $rootScope.$broadcast('history_focus', {peerString: peerString}); + }); + }); + }; + + $scope.delete = function () { + var messageID = $scope.messageID; + ErrorService.confirm({type: 'MESSAGE_DELETE'}).then(function () { + AppMessagesManager.deleteMessages([messageID]); + }); + }; + + + $scope.$on('history_delete', function (e, historyUpdate) { + console.log(dT(), 'delete', historyUpdate); + if (historyUpdate.peerID == peerID) { + if (historyUpdate.msgs[$scope.messageID]) { + if ($scope.nav.hasNext) { + $scope.nav.next(); + } else if ($scope.nav.hasPrev) { + $scope.nav.prev(); + } else { + return $modalInstance.dismiss(); + } + } + var newList = []; + for (var i = 0; i < list.length; i++) { + if (!historyUpdate.msgs[list[i]]) { + newList.push(list[i]); + } + }; + list = newList; + } + }); + }) .controller('VideoModalController', function ($scope, AppVideoManager) { diff --git a/app/js/directives.js b/app/js/directives.js index 601a4925..81d1a3a1 100644 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -683,19 +683,18 @@ angular.module('myApp.directives', ['myApp.filters']) link: link, transclude: true, template: - '
\ + '
\
\ -
\ -
\ -
\ - {{progress.percent}}% Complete (success)\ +
\ +
\ +
\
\
\
\
\ \
\ @@ -711,53 +710,68 @@ angular.module('myApp.directives', ['myApp.filters']) function link ($scope, element, attrs) { - var imgElement = $('img', element); + var imgElement = $('img', element)[0], + resizeElements = $('.img_fullsize_with_progress_wrap', element) + .add('.img_fullsize_progress_wrap', element) + .add($(imgElement)), + resize = function () { + resizeElements.css({width: $scope.fullPhoto.width, height: $scope.fullPhoto.height}); + $scope.$emit('ui_height'); + }; - imgElement - .attr('src', MtpApiFileManager.getCachedFile($scope.thumbLocation) || 'img/blank.gif') - .addClass('thumb_blurred') - .addClass('thumb_blur_animation'); + var jump = 0; + $scope.$watchCollection('fullPhoto.location', function () { + var cachedSrc = MtpApiFileManager.getCachedFile($scope.thumbLocation), + curJump = ++jump; - if (!$scope.fullPhoto.location) { - return; - } + if (cachedSrc) { + imgElement.src = cachedSrc; + resize(); + } else { + imgElement.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + } + if (!$scope.fullPhoto.location) { + return; + } - var apiPromise; - if ($scope.fullPhoto.size) { - var inputLocation = { - _: 'inputFileLocation', - volume_id: $scope.fullPhoto.location.volume_id, - local_id: $scope.fullPhoto.location.local_id, - secret: $scope.fullPhoto.location.secret - }; - apiPromise = MtpApiFileManager.downloadFile($scope.fullPhoto.location.dc_id, inputLocation, $scope.fullPhoto.size); - } else { - apiPromise = MtpApiFileManager.downloadSmallFile($scope.fullPhoto.location); - } + var apiPromise; + if ($scope.fullPhoto.size) { + var inputLocation = { + _: 'inputFileLocation', + volume_id: $scope.fullPhoto.location.volume_id, + local_id: $scope.fullPhoto.location.local_id, + secret: $scope.fullPhoto.location.secret + }; + apiPromise = MtpApiFileManager.downloadFile($scope.fullPhoto.location.dc_id, inputLocation, $scope.fullPhoto.size); + } else { + apiPromise = MtpApiFileManager.downloadSmallFile($scope.fullPhoto.location); + } - $scope.progress = {enabled: true, percent: 1}; + $scope.progress = {enabled: true, percent: 0}; - apiPromise.then(function (url) { - $scope.progress.enabled = false; - imgElement - .attr('src', url) - .removeClass('thumb_blurred'); + apiPromise.then(function (url) { + if (curJump == jump) { + $scope.progress.enabled = false; + imgElement.src = url; + resize(); + } + }, function (e) { + console.log('Download image failed', e, $scope.fullPhoto.location); + $scope.progress.enabled = false; - }, function (e) { - console.log('Download image failed', e, $scope.fullPhoto.location); - $scope.progress.enabled = false; + if (e && e.type == 'FS_BROWSER_UNSUPPORTED') { + $scope.error = {html: 'Your browser doesn\'t support LocalFileSystem feature which is needed to display this image.
Please, install Google Chrome or use mobile app instead.'}; + } else { + $scope.error = {text: 'Download failed', error: e}; + } + }, function (progress) { + $scope.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total)); + }); + }) - if (e && e.type == 'FS_BROWSER_UNSUPPORTED') { - $scope.error = {html: 'Your browser doesn\'t support LocalFileSystem feature which is needed to display this image.
Please, install Google Chrome or use mobile app instead.'}; - } else { - $scope.error = {text: 'Download failed', error: e}; - } - }, function (progress) { - $scope.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total)); - }); + resize(); } - }) @@ -771,9 +785,7 @@ angular.module('myApp.directives', ['myApp.filters'])
\
\
\ -
\ - {{progress.percent}}% Complete (success)\ -
\ +
\
\
\
\ @@ -975,6 +987,44 @@ angular.module('myApp.directives', ['myApp.filters']) }) + .directive('myModalNav', function () { + + return { + link: link + }; + + function link($scope, element, attrs) { + + var onKeyDown = function (event) { + var target = event.target; + if (target && (target.tagName == 'INPUT' || target.tagName == 'TEXTAREA')) { + return false; + } + + switch (event.keyCode) { + case 39: // right + case 32: // space + case 34: // pg down + case 40: // down + $scope.$eval(attrs.next); + break; + + case 37: // left + case 33: // pg up + case 38: // up + $scope.$eval(attrs.prev); + break; + } + }; + + $(document).on('keydown', onKeyDown); + + $scope.$on('$destroy', function () { + $(document).off('keydown', onKeyDown); + }); + }; + }) + .directive('myModalPosition', function ($window, $timeout) { @@ -993,9 +1043,12 @@ angular.module('myApp.directives', ['myApp.filters']) } else { $(element[0].parentNode).css('marginTop', ''); } - $timeout(function () { - $(element[0].parentNode).addClass('modal-content-animated'); - }, 300); + + if (attrs.animation != 'no') { + $timeout(function () { + $(element[0].parentNode).addClass('modal-content-animated'); + }, 300); + } }; onContentLoaded(updateMargin); diff --git a/app/js/lib/mtproto.js b/app/js/lib/mtproto.js index 024b6eaa..6a335219 100644 --- a/app/js/lib/mtproto.js +++ b/app/js/lib/mtproto.js @@ -638,6 +638,8 @@ TLSerialization.prototype.storeMethod = function (methodName, params) { angular.forEach(methodData.params, function (param) { self.storeObject(params[param.name], param.type, methodName + '[' + param.name + ']'); }); + + return methodData.type; }; TLSerialization.prototype.storeObject = function (obj, type, field) { @@ -707,6 +709,8 @@ TLSerialization.prototype.storeObject = function (obj, type, field) { angular.forEach(constructorData.params, function (param) { self.storeObject(obj[param.name], param.type, field + '[' + predicate + '][' + param.name + ']'); }); + + return constructorData.type; }; @@ -715,6 +719,7 @@ function TLDeserialization (buffer, options) { options = options || {}; this.offset = 0; // in bytes + this.override = options.override || {}; this.buffer = buffer; this.intView = new Uint32Array(this.buffer); @@ -961,12 +966,16 @@ TLDeserialization.prototype.fetchObject = function (type, field) { predicate = constructorData.predicate; - var result = {'_': predicate}; + var result = {'_': predicate}, + self = this; - var self = this; - angular.forEach(constructorData.params, function (param) { - result[param.name] = self.fetchObject(param.type, field + '[' + predicate + '][' + param.name + ']'); - }); + if (this.override[predicate]) { + this.override[predicate].apply(this, [result, field + '[' + predicate + ']']); + } else { + angular.forEach(constructorData.params, function (param) { + result[param.name] = self.fetchObject(param.type, field + '[' + predicate + '][' + param.name + ']'); + }); + } if (fallback) { this.mtproto = true; @@ -1773,7 +1782,7 @@ factory('MtpNetworkerFactory', function (MtpDcConfigurator, MtpMessageIdGenerato serializer.storeLong(options.afterMessageID, 'msg_id'); } - serializer.storeMethod(method, params); + options.resultType = serializer.storeMethod(method, params); var messageID = MtpMessageIdGenerator.generateID(), seqNo = this.generateSeqNo(), @@ -2215,7 +2224,36 @@ factory('MtpNetworkerFactory', function (MtpDcConfigurator, MtpMessageIdGenerato } var buffer = bytesToArrayBuffer(messageBody); - var deserializer = new TLDeserialization(buffer, {mtproto: true}); + var deserializerOptions = { + mtproto: true, + override: { + message: function (result, field) { + result.msg_id = this.fetchLong(field + '[msg_id]'); + result.seqno = this.fetchInt(field + '[seqno]'); + result.bytes = this.fetchInt(field + '[bytes]'); + + var offset = this.getOffset(); + + try { + result.body = this.fetchObject('Object', field + '[body]'); + } catch (e) { + result.body = {_: 'parse_error', error: e}; + } + this.offset = offset + result.bytes; + // console.log(dT(), 'override message', result); + }, + rpc_result: function (result, field) { + result.req_msg_id = this.fetchLong(field + '[req_msg_id]'); + + var sentMessage = self.sentMessages[result.req_msg_id], + type = sentMessage && sentMessage.resultType || 'Object'; + + result.result = this.fetchObject(type, field + '[result]'); + // console.log(dT(), 'override rpc_result', type, result); + } + } + }; + var deserializer = new TLDeserialization(buffer, deserializerOptions); var response = deserializer.fetchObject('', 'INPUT'); diff --git a/app/js/services.js b/app/js/services.js index 44fc373f..40149691 100644 --- a/app/js/services.js +++ b/app/js/services.js @@ -886,6 +886,54 @@ angular.module('myApp.services', []) } function getSearch (inputPeer, query, inputFilter, maxID, limit) { + var foundMsgs = []; + + if (!maxID && !query) { + var peerID = AppPeersManager.getPeerID(inputPeer), + historyStorage = historiesStorage[peerID]; + + if (historyStorage !== undefined && historyStorage.history.length) { + var neededContents = {}, + neededLimit = limit || 20, + i, message; + + switch (inputFilter._) { + case 'inputMessagesFilterPhotos': + neededContents['messageMediaPhoto'] = true; + break; + + case 'inputMessagesFilterVideo': + neededContents['messageMediaVideo'] = true; + break; + + case 'inputMessagesFilterPhotoVideo': + neededContents['messageMediaPhoto'] = true; + neededContents['messageMediaVideo'] = true; + break; + + case 'inputMessagesFilterDocument': + neededContents['messageMediaDocument'] = true; + break; + } + for (i = 0; i < historyStorage.history.length; i++) { + message = messagesStorage[historyStorage.history[i]]; + if (message.media && neededContents[message.media._]) { + foundMsgs.push(message.id); + if (foundMsgs.length >= neededLimit) { + break; + } + } + } + } + } + + if (foundMsgs.length || limit == 1000) { + return $q.when({ + count: null, + history: foundMsgs + }); + } + return MtpApiManager.invokeApi('messages.search', { peer: inputPeer, q: query || '', @@ -903,7 +951,7 @@ angular.module('myApp.services', []) ? searchResult.count : searchResult.messages.length; - var foundMsgs = []; + foundMsgs = []; angular.forEach(searchResult.messages, function (message) { foundMsgs.push(message.id); }); @@ -915,6 +963,10 @@ angular.module('myApp.services', []) }); } + function getMessage (messageID) { + return messagesStorage[messageID] || {deleted: true}; + } + function deleteMessages (messageIDs) { return MtpApiManager.invokeApi('messages.deleteMessages', { id: messageIDs @@ -1874,6 +1926,7 @@ angular.module('myApp.services', []) getDialogs: getDialogs, getHistory: getHistory, getSearch: getSearch, + getMessage: getMessage, readHistory: readHistory, flushHistory: flushHistory, deleteMessages: deleteMessages, @@ -2011,6 +2064,8 @@ angular.module('myApp.services', []) full.height = fullPhotoSize.h; } + full.modalWidth = Math.max(full.width, Math.min(400, fullWidth)); + full.location = fullPhotoSize.location; full.size = fullPhotoSize.size; } diff --git a/app/partials/confirm_modal.html b/app/partials/confirm_modal.html index 80143612..e551d89c 100644 --- a/app/partials/confirm_modal.html +++ b/app/partials/confirm_modal.html @@ -14,9 +14,8 @@ when="{'one': 'Are you sure to send file from clipboard?', 'other': 'Are you sure to send {} files from clipboard?'}"> - - Are you sure to send file(s) from clipboard? - + Are you sure to send file(s) from clipboard? + Are you sure to delete the message?
@@ -31,6 +30,7 @@ Delete Chat Send Send + Delete OK
diff --git a/app/partials/message.html b/app/partials/message.html index 983a5bcd..c6fefd44 100644 --- a/app/partials/message.html +++ b/app/partials/message.html @@ -92,7 +92,7 @@
- + +
diff --git a/app/partials/video_modal.html b/app/partials/video_modal.html index ac9f6d05..280f23a4 100644 --- a/app/partials/video_modal.html +++ b/app/partials/video_modal.html @@ -4,7 +4,14 @@
-

From: , {{video.date | dateOrTime}}

+
+ Forward + Delete +
+ +

+ , {{video.date | dateOrTime}} +

diff --git a/app/vendor/ui-bootstrap/ui-bootstrap-custom-tpls-0.10.0.js b/app/vendor/ui-bootstrap/ui-bootstrap-custom-tpls-0.10.0.js index bef93ffb..6488a3dc 100644 --- a/app/vendor/ui-bootstrap/ui-bootstrap-custom-tpls-0.10.0.js +++ b/app/vendor/ui-bootstrap/ui-bootstrap-custom-tpls-0.10.0.js @@ -300,7 +300,8 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) restrict: 'EA', scope: { index: '@', - animate: '=' + animate: '=', + nav: '=' }, replace: true, transclude: true, @@ -447,6 +448,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) angularDomEl.attr('window-class', modal.windowClass); angularDomEl.attr('index', openedWindows.length() - 1); angularDomEl.attr('animate', 'animate'); + angularDomEl.attr('nav', 'nav'); angularDomEl.html(modal.content); var modalDomEl = $compile(angularDomEl)(modal.scope); @@ -1074,6 +1076,9 @@ angular.module("template/modal/backdrop.html", []).run(["$templateCache", functi angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { $templateCache.put("template/modal/window.html", "
\n" + + "
\n" + + "
\n" + + "
\n" + "
\n" + "
\n" + "
\n" + diff --git a/webogram.sublime-project b/webogram.sublime-project index b5b7952f..87cf2142 100644 --- a/webogram.sublime-project +++ b/webogram.sublime-project @@ -5,7 +5,7 @@ "follow_symlinks": true, "path": ".", "folder_exclude_patterns": ["*dist", "node_modules", "releases"], - "file_exclude_patterns": ["*.zip"] + "file_exclude_patterns": ["*.zip", "templates.js"] } ] }