diff --git a/README.md b/README.md index a81143be..d50ad64d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## [Webogram](http://zhukov.github.io/webogram) - Telegram UNOFFICIAL web app +## [Webogram](http://zhukov.github.io/webogram) – UNOFFICIAL Telegram Web App Telegram offers great [apps for mobile communication](https://www.telegram.org). It is based on the [MTProto protocol](https://core.telegram.org/protocol) and has an [Open API](http://core.telegram.org/api). I personally like Telegram for its speed and cloud-support (that makes a web app possible, unlike in the case of WA and others). diff --git a/app/css/app.css b/app/css/app.css index c16bef1b..5675d5ef 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -709,7 +709,8 @@ div.im_message_video_thumb { -.im_message_document { +.im_message_document, +.im_message_upload_file { margin-top: 3px; border-radius: 3px; display: inline-block; @@ -728,7 +729,7 @@ div.im_message_video_thumb { .im_message_document .icon-group { background-image: url(../img/icons/DialogListGroupChatIcon_Highlighted@2x.png); } -.im_message_document .im_message_document_name { +.im_message_document_name { color: #999; vertical-align: text-top; display: inline-block; @@ -756,6 +757,33 @@ div.im_message_video_thumb { padding-right: 3px; } +.im_message_upload_progress_wrap, +.im_message_download_progress_wrap { + margin-top: 5px; +} + +.tg_up_progress, +.tg_down_progress { + height: 5px; + margin: 0; + padding: 0; + background: rgba(0,0,0, 0.1); + border: 0; + border-radius: 4px; +} +.tg_up_progress .progress-bar, +.tg_down_progress .progress-bar { + height: 5px; + line-height: 5px; + background: #43A4DB; + border-radius: 3px; + overflow: hidden; +} +.tg_down_progress .progress-bar { + background: #6DBF69; +} + + .im_service_message_wrap { text-align: center; @@ -782,7 +810,7 @@ div.im_message_video_thumb { .im_content_message_wrap { margin: 10px 0 5px; } -.icon-message-status-unread { +.icon-message-status { background: #43A4DB; border: 1px solid #FFF; display: block; @@ -793,7 +821,21 @@ div.im_message_video_thumb { position: absolute; margin-left: -27px; margin-top: 14px; + opacity: 0; + + -webkit-transition: opacity ease-in-out 0.15s; + transition: opacity ease-in-out 0.15s; } +.icon-message-status-unread { + opacity: 1.0; +} +.icon-message-status-pending { + opacity: 0.5; +} +/*.icon-message-status-done { + opacity: 0; +}*/ + .icon-message-status-tick { /*display: inline-block;*/ display: none; diff --git a/app/js/controllers.js b/app/js/controllers.js index 15c8bcdd..b973eb71 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -301,6 +301,8 @@ angular.module('myApp.controllers', []) var typingTimeouts = {}; + $scope.$on('history_update', angular.noop); + $scope.$on('history_append', function (e, addedMessage) { if (addedMessage.peerID == $scope.curDialog.peerID) { dLog('append', addedMessage); @@ -383,7 +385,7 @@ angular.module('myApp.controllers', []) var text = $scope.draftMessage.text; - if ($scope.draftMessage.sending || !text.length) { + if (!text.length) { return false; } @@ -395,50 +397,16 @@ angular.module('myApp.controllers', []) return all; }); - $scope.draftMessage.sending = true; - - MtpApiManager.invokeApi('messages.sendMessage', { - peer: $scope.curDialog.inputPeer, - message: text, - random_id: $scope.draftMessage.randomID - }).then(function (result) { - - if (ApiUpdatesManager.saveSeq(result.seq)) { - - MtpApiManager.getUserID().then(function (fromID) { - ApiUpdatesManager.saveUpdate({ - _: 'updateNewMessage', - message: { - _: 'message', - id: result.id, - from_id: fromID, - to_id: AppPeersManager.getOutputPeer($scope.curDialog.peerID), - out: true, - unread: true, - date: result.date, - message: text, - media: {_: 'messageMediaEmpty'} - }, - pts: result.pts - }); - }); - - } + AppMessagesManager.sendText($scope.curDialog.peerID, text); + resetDraft(); + $scope.$broadcast('ui_message_send'); - $scope.$broadcast('ui_message_send'); - - resetDraft(); - }, function () { - delete $scope.draftMessage.sending; - }); - - return cancelEvent(e); + return false; } function resetDraft () { $scope.draftMessage = { - randomID: [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)], text: '' }; } @@ -449,35 +417,8 @@ angular.module('myApp.controllers', []) } for (var i = 0; i < newVal.length; i++) { - (function (file, randomID) { - MtpApiFileManager.uploadFile(file).then(function (inputFile) { - var inputMedia; - if (file.type == 'image/jpeg') { - inputMedia = {_: 'inputMediaUploadedPhoto', file: inputFile}; - } else { - inputMedia = {_: 'inputMediaUploadedDocument', file: inputFile, file_name: file.name, mime_type: file.type}; - } - MtpApiManager.invokeApi('messages.sendMedia', { - peer: $scope.curDialog.inputPeer, - media: inputMedia, - random_id: randomID - }).then(function (result) { - - if (ApiUpdatesManager.saveSeq(result.seq)) { - ApiUpdatesManager.saveUpdate({ - _: 'updateNewMessage', - message: result.message, - pts: result.pts - }); - } - - $scope.$broadcast('ui_message_send'); - }); - }, function (error) { - dLog('upload error', error); - }) - - })(newVal[i], [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)]); + AppMessagesManager.sendFile($scope.curDialog.peerID, newVal[i]); + $scope.$broadcast('ui_message_send'); } } diff --git a/app/js/directives.js b/app/js/directives.js index 482a445e..7aad2f64 100644 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -450,7 +450,8 @@ angular.module('myApp.directives', ['myApp.filters']) \ \
\ -
\ @@ -471,9 +472,19 @@ angular.module('myApp.directives', ['myApp.filters']) access_hash: scope.video.access_hash }; + var hasQt = false, i; + // if (navigator.plugins) { + // for (i = 0; i < navigator.plugins.length; i++) { + // if (navigator.plugins[i].name.indexOf('QuickTime') >= 0) { + // hasQt = true; + // } + // } + // } + MtpApiFileManager.downloadFile(scope.video.dc_id, inputLocation, scope.video.size).then(function (url) { scope.progress.enabled = false; // scope.progress = {enabled: true, percent: 50}; + scope.player.quicktime = hasQt; scope.player.src = $sce.trustAsResourceUrl(url); }, function (e) { dLog('Download image failed', e, scope.fullPhoto.location); @@ -510,3 +521,33 @@ angular.module('myApp.directives', ['myApp.filters']) } }) + + + .directive('myTypingDots', function($interval) { + + return { + link: link, + }; + + var interval; + + function link (scope, element, attrs) { + var promise = $interval(function () { + var time = +new Date(), + cnt = 3; + + if (time % 1000 <= 200) { + cnt = 0; + } else if (time % 1000 <= 400) { + cnt = 1; + } else if (time % 1000 <= 600) { + cnt = 2; + } + element.html((new Array(cnt + 1)).join('.')); + }, 200); + + scope.$on('$destroy', function cleanup() { + $interval.cancel(promise); + }); + } + }) diff --git a/app/js/lib/mtproto.js b/app/js/lib/mtproto.js index 0fa9230d..43e1b44d 100644 --- a/app/js/lib/mtproto.js +++ b/app/js/lib/mtproto.js @@ -2130,7 +2130,7 @@ factory('MtpApiManager', function (AppConfigManager, MtpAuthorizer, MtpNetworker deferred.resolve(result); // setTimeout(function () { // deferred.resolve(result); - // },1000); + // }, 1000); }, function (error) { dLog('error', error.code, error.type, baseDcID, dcID); @@ -2481,7 +2481,7 @@ factory('MtpApiFileManager', function (MtpApiManager, $q, $window) { cachedFS.root.getFile(fileName, {create: false}, function(fileEntry) { fileEntry.file(function(file) { dLog('check size', file.size, size); - if (file.size >= size && false) { + if (file.size >= size) { deferred.resolve(fileEntry.toURL()); } else { dLog('File bad size', file, size); diff --git a/app/js/services.js b/app/js/services.js index f2569107..7c27184f 100644 --- a/app/js/services.js +++ b/app/js/services.js @@ -405,12 +405,15 @@ angular.module('myApp.services', []) } }) -.service('AppMessagesManager', function ($q, $rootScope, $filter, $sanitize, ApiUpdatesManager, AppUsersManager, AppChatsManager, AppPeersManager, AppPhotosManager, AppVideoManager, AppDocsManager, MtpApiManager, RichTextProcessor) { +.service('AppMessagesManager', function ($q, $rootScope, $filter, $sanitize, ApiUpdatesManager, AppUsersManager, AppChatsManager, AppPeersManager, AppPhotosManager, AppVideoManager, AppDocsManager, MtpApiManager, MtpApiFileManager, RichTextProcessor) { var messagesStorage = {}; var messagesForHistory = {}; var historiesStorage = {}; var dialogsStorage = {count: null, dialogs: []}; + var pendingByRandomID = {}; + var pendingByMessageID = {}; + var tempID = -1; function getDialogs (offset, limit) { if (dialogsStorage.count !== null && dialogsStorage.dialogs.length >= offset + limit) { @@ -458,10 +461,14 @@ angular.module('myApp.services', []) var peerID = AppPeersManager.getPeerID(inputPeer), historyStorage = historiesStorage[peerID], - offset = 0; + offset = 0, + resultPending = []; if (historyStorage === undefined) { - historyStorage = historiesStorage[peerID] = {count: null, history: []}; + historyStorage = historiesStorage[peerID] = {count: null, history: [], pending: []}; + } + else if (!maxID && historyStorage.pending.length) { + resultPending = historyStorage.pending.slice(); } @@ -477,7 +484,7 @@ angular.module('myApp.services', []) if (historyStorage.count !== null && historyStorage.history.length >= offset + limit) { return $q.when({ count: historyStorage.count, - history: historyStorage.history.slice(offset, offset + limit) + history: resultPending.concat(historyStorage.history.slice(offset, offset + limit)) }); } @@ -515,7 +522,7 @@ angular.module('myApp.services', []) deferred.resolve({ count: historyStorage.count, - history: historyStorage.history.slice(offset, offset + limit) + history: resultPending.concat(historyStorage.history.slice(offset, offset + limit)) }); }, function (error) { deferred.reject(error); @@ -619,6 +626,205 @@ angular.module('myApp.services', []) }); } + function sendText(peerID, text) { + 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), + message; + + if (historyStorage === undefined) { + historyStorage = historiesStorage[peerID] = {count: null, history: [], pending: []}; + } + + MtpApiManager.getUserID().then(function (fromID) { + message = { + _: 'message', + id: messageID, + from_id: fromID, + to_id: AppPeersManager.getOutputPeer(peerID), + out: true, + unread: true, + date: (+new Date()) / 1000, + message: text, + media: {_: 'messageMediaEmpty'}, + random_id: randomIDS, + pending: true + }; + + message.send = function () { + MtpApiManager.invokeApi('messages.sendMessage', { + peer: inputPeer, + message: text, + random_id: randomID + }).then(function (result) { + if (ApiUpdatesManager.saveSeq(result.seq)) { + ApiUpdatesManager.saveUpdate({ + _: 'updateMessageID', + random_id: randomIDS, + id: result.id + }); + + message.date = result.date; + message.id = result.id; + ApiUpdatesManager.saveUpdate({ + _: 'updateNewMessage', + message: message, + pts: result.pts + }); + } + }); + }; + + saveMessages([message]); + historyStorage.pending.unshift(messageID); + $rootScope.$broadcast('history_append', {peerID: peerID, messageID: messageID}); + + // setTimeout(function () { + message.send(); + // }, 5000); + }); + + pendingByRandomID[randomIDS] = [peerID, messageID]; + }; + + function sendFile(peerID, file, 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), + isPhoto = file.type == 'image/jpeg' && file.size <= 5242880; // 5Mb + + if (historyStorage === undefined) { + historyStorage = historiesStorage[peerID] = {count: null, history: [], pending: []}; + } + + MtpApiManager.getUserID().then(function (fromID) { + var media = { + _: 'messageMediaPending', + type: isPhoto ? 'photo' : 'doc', + file_name: file.name, + size: file.size, + progress: {percent: 1} + }; + + var message = { + _: 'message', + id: messageID, + from_id: fromID, + to_id: AppPeersManager.getOutputPeer(peerID), + out: true, + unread: true, + date: (+new Date()) / 1000, + message: '', + media: media, + random_id: randomIDS, + pending: true + }; + + message.send = function () { + MtpApiFileManager.uploadFile(file).then(function (inputFile) { + var inputMedia; + if (isPhoto) { + inputMedia = {_: 'inputMediaUploadedPhoto', file: inputFile}; + } else { + inputMedia = {_: 'inputMediaUploadedDocument', file: inputFile, file_name: file.name, mime_type: file.type}; + } + MtpApiManager.invokeApi('messages.sendMedia', { + peer: inputPeer, + media: inputMedia, + random_id: randomID + }).then(function (result) { + if (ApiUpdatesManager.saveSeq(result.seq)) { + ApiUpdatesManager.saveUpdate({ + _: 'updateMessageID', + random_id: randomIDS, + id: result.message.id + }); + + message.date = result.message.date; + message.id = result.message.id; + message.media = result.message.media; + + ApiUpdatesManager.saveUpdate({ + _: 'updateNewMessage', + message: message, + pts: result.pts + }); + } + + }); + }, null, function (progress) { + // dLog('upload progress', progress); + var historyMessage = messagesForHistory[messageID], + percent = Math.max(1, Math.floor(100 * progress.done / progress.total)); + + media.progress.percent = percent; + if (historyMessage) { + historyMessage.media.progress.percent = percent; + $rootScope.$broadcast('history_update', {peerID: peerID}); + } + }); + }; + + saveMessages([message]); + historyStorage.pending.unshift(messageID); + $rootScope.$broadcast('history_append', {peerID: peerID, messageID: messageID}); + + // setTimeout(function () { + message.send(); + // }, 5000); + }); + + pendingByRandomID[randomIDS] = [peerID, messageID]; + } + + + function finalizePendingMessage(randomID, finalMessage) { + var pendingData = pendingByRandomID[randomID]; + // dLog('pdata', randomID, pendingData); + + if (pendingData) { + var peerID = pendingData[0], + tempID = pendingData[1], + historyStorage = historiesStorage[peerID], + index = false, + message = false, + historyMessage = false, + i; + + // dLog('pending', randomID, historyStorage.pending); + for (i = 0; i < historyStorage.pending.length; i++) { + if (historyStorage.pending[i] == tempID) { + historyStorage.pending.splice(i, 1); + break; + } + } + + if (message = messagesStorage[tempID]) { + delete message.pending; + delete message.random_id; + delete message.send; + } + + if (historyMessage = messagesForHistory[tempID]) { + messagesForHistory[finalMessage.id] = angular.extend(historyMessage, wrapForHistory(finalMessage.id)); + delete historyMessage.pending; + delete historyMessage.random_id; + delete historyMessage.send; + } + + delete messagesForHistory[tempID]; + delete messagesStorage[tempID]; + + return message; + } + + return false; + } + function getMessagePeer (message) { var toID = message.to_id && AppPeersManager.getPeerID(message.to_id) || 0; @@ -660,8 +866,8 @@ angular.module('myApp.services', []) return message; } - function wrapForHistory (msgID) { - if (messagesForHistory[msgID] !== undefined) { + function wrapForHistory (msgID, force) { + if (!force && messagesForHistory[msgID] !== undefined) { return messagesForHistory[msgID]; } @@ -679,6 +885,10 @@ angular.module('myApp.services', []) case 'messageMediaVideo': message.media.video = AppVideoManager.wrapForHistory(message.media.video.id); break; + + case 'messageMediaDocument': + message.media.document = AppDocsManager.wrapForHistory(message.media.document.id); + break; } if (message.media.user_id) { @@ -714,10 +924,13 @@ angular.module('myApp.services', []) return []; } - $rootScope.$on('apiUpdate', function (e, update) { dLog('on apiUpdate', update); switch (update._) { + case 'updateMessageID': + pendingByMessageID[update.id] = update.random_id; + break; + case 'updateNewMessage': var message = update.message, peerID = getMessagePeer(message), @@ -729,7 +942,7 @@ angular.module('myApp.services', []) return false; } } else { - historyStorage = historiesStorage[peerID] = {count: null, history: []}; + historyStorage = historiesStorage[peerID] = {count: null, history: [], pending: []}; } saveMessages([message]); @@ -737,8 +950,23 @@ angular.module('myApp.services', []) if (historyStorage.count !== null) { historyStorage.count++; } + historyStorage.history.unshift(message.id); - $rootScope.$broadcast('history_append', {peerID: peerID, messageID: message.id}); + var randomID = pendingByMessageID[message.id], + pendingMessage; + + + if (randomID) { + if (pendingMessage = finalizePendingMessage(randomID, message)) { + $rootScope.$broadcast('history_update', {peerID: peerID}); + } + delete pendingByMessageID[message.id]; + } + + // dLog(11, randomID, pendingMessage); + if (!pendingMessage) { + $rootScope.$broadcast('history_append', {peerID: peerID, messageID: message.id}); + } var foundDialog = getDialogByPeerID(peerID), dialog; @@ -767,6 +995,7 @@ angular.module('myApp.services', []) if (message) { message.unread = false; if (messagesForHistory[messageID]) { + // dLog(222, messagesForHistory[messageID]); messagesForHistory[messageID].unread = false; } peerID = getMessagePeer(message); @@ -782,6 +1011,7 @@ angular.module('myApp.services', []) angular.forEach(dialogsUpdated, function(count, peerID) { $rootScope.$broadcast('dialog_unread', {peerID: peerID, count: count}); }); + // $rootScope.$broadcast('history_update'); break; } }); @@ -791,6 +1021,8 @@ angular.module('myApp.services', []) getHistory: getHistory, readHistory: readHistory, saveMessages: saveMessages, + sendText: sendText, + sendFile: sendFile, getMessagePeer: getMessagePeer, wrapForDialog: wrapForDialog, wrapForHistory: wrapForHistory @@ -1011,6 +1243,7 @@ angular.module('myApp.services', []) .service('AppDocsManager', function ($rootScope, $modal, $window, $timeout, MtpApiFileManager, AppUsersManager) { var docs = {}; + var docsForHistory = {}; function saveDoc (apiDoc) { docs[apiDoc.id] = apiDoc; @@ -1026,6 +1259,10 @@ angular.module('myApp.services', []) }; function wrapForHistory (docID) { + if (docsForHistory[docID] !== undefined) { + return docsForHistory[docID]; + } + var doc = angular.copy(docs[docID]), width = 100, height = 100, @@ -1045,23 +1282,31 @@ angular.module('myApp.services', []) thumb.location = thumbPhotoSize.location; thumb.size = thumbPhotoSize.size; + } else { + thumb = false; } doc.thumb = thumb; - return doc; + return docsForHistory[docID] = doc; } function openDoc (docID, accessHash) { var doc = docs[docID], + historyDoc = docsForHistory[docID] || doc || {}, inputFileLocation = { _: 'inputDocumentFileLocation', id: docID, access_hash: accessHash || doc.access_hash - }, - scope = {}; + }; + + historyDoc.progress = {enabled: true, percent: 1}; - scope.progress = {enabled: true, percent: 1}; + function updateDownloadProgress (progress) { + dLog('dl progress', progress); + historyDoc.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total)); + $rootScope.$broadcast('history_update'); + } if (window.chrome && chrome.fileSystem && chrome.fileSystem.chooseEntry) { @@ -1076,13 +1321,16 @@ angular.module('myApp.services', []) }] }, function (writableFileEntry) { MtpApiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, writableFileEntry).then(function (url) { + delete historyDoc.progress; dLog('file save done'); - }); - + }, function (e) { + dLog('document download failed', e); + historyDoc.progress.enabled = false; + }, updateDownloadProgress); }); } else { MtpApiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size).then(function (url) { - scope.progress.enabled = false; + delete historyDoc.progress; var a = $('Download').attr('href', url).attr('target', '_blank').attr('download', doc.file_name).appendTo('body'); a[0].dataset.downloadurl = ['png', doc.file_name, url].join(':'); @@ -1092,9 +1340,8 @@ angular.module('myApp.services', []) }, 100); }, function (e) { dLog('document download failed', e); - }, function (progress) { - scope.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total)); - }); + historyDoc.progress.enabled = false; + }, updateDownloadProgress); } } @@ -1240,6 +1487,11 @@ angular.module('myApp.services', []) AppUsersManager.saveApiUsers(differenceResult.users); AppChatsManager.saveApiChats(differenceResult.chats); + // Should be first because of updateMessageID + angular.forEach(differenceResult.other_updates, function(update){ + saveUpdate(update, true); + }); + angular.forEach(differenceResult.new_messages, function (apiMessage) { saveUpdate({ _: 'updateNewMessage', @@ -1248,10 +1500,6 @@ angular.module('myApp.services', []) }, true); }); - angular.forEach(differenceResult.other_updates, function(update){ - saveUpdate(update, true); - }); - var nextState = differenceResult.intermediate_state || differenceResult.state; curState.seq = nextState.seq; curState.pts = nextState.pts; diff --git a/app/partials/head.html b/app/partials/head.html index 66c5f874..d0299ffb 100644 --- a/app/partials/head.html +++ b/app/partials/head.html @@ -2,7 +2,7 @@