/*! * Webogram v0.1 - messaging web application for MTProto * https://github.com/zhukov/webogram * Copyright (C) 2014 Igor Zhukov * https://github.com/zhukov/webogram/blob/master/LICENSE */ 'use strict'; /* Services */ angular.module('myApp.services', []) .service('AppConfigManager', function ($q) { var testPrefix = window._testMode ? 't_' : ''; var cache = {}; var useLs = !window.chrome || !chrome.storage || !chrome.storage.local; function getValue() { var keys = Array.prototype.slice.call(arguments), result = [], single = keys.length == 1, allFound = true; for (var i = 0; i < keys.length; i++) { keys[i] = testPrefix + keys[i]; } angular.forEach(keys, function (key) { if (cache[key] !== undefined) { result.push(cache[key]); } else if (useLs) { var value = localStorage.getItem(key); value = (value === undefined || value === null) ? false : JSON.parse(value); result.push(cache[key] = value); } else { allFound = false; } }); if (allFound) { return $q.when(single ? result[0] : result); } var deferred = $q.defer(); // dLog('get', keys); chrome.storage.local.get(keys, function (resultObj) { // dLog('got', resultObj); result = []; angular.forEach(keys, function (key) { var value = resultObj[key]; // dLog('p1', key, value); value = value === undefined || value === null ? false : JSON.parse(value); // dLog('p2', value); result.push(cache[key] = value); }); // dLog('got parsed', result); deferred.resolve(single ? result[0] : result); }); return deferred.promise; }; function setValue(obj) { var keyValues = {}; angular.forEach(obj, function (value, key) { keyValues[testPrefix + key] = JSON.stringify(value); cache[testPrefix + key] = value; }); if (useLs) { angular.forEach(keyValues, function (value, key) { localStorage.setItem(key, value); }); return $q.when(); } var deferred = $q.defer(); chrome.storage.local.set(keyValues, function () { deferred.resolve(); }); return deferred.promise; }; function removeValue () { var keys = Array.prototype.slice.call(arguments); for (var i = 0; i < keys.length; i++) { keys[i] = testPrefix + keys[i]; } angular.forEach(keys, function(key){ delete cache[key]; }); if (useLs) { angular.forEach(keys, function(key){ localStorage.removeItem(key); }); return $q.when(); } var deferred = $q.defer(); chrome.storage.local.remove(keys, function () { deferred.resolve(); }); return deferred.promise; }; return { get: getValue, set: setValue, remove: removeValue }; }) .service('AppUsersManager', function ($rootScope, $modal, MtpApiFileManager, MtpApiManager, RichTextProcessor) { var users = {}; function saveApiUsers (apiUsers) { angular.forEach(apiUsers, saveApiUser); }; function saveApiUser (apiUser) { if (!angular.isObject(apiUser)) { return; } 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}); } else { apiUser.rFirstName = RichTextProcessor.wrapRichText(apiUser.last_name, {noLinks: true, noLinebreaks: true}) || 'DELETED'; apiUser.rFullName = RichTextProcessor.wrapRichText(apiUser.last_name, {noLinks: true, noLinebreaks: true}) || 'DELETED'; } if (users[apiUser.id] === undefined) { users[apiUser.id] = apiUser; } else { angular.extend(users[apiUser.id], apiUser); } }; function getUser (id) { if (angular.isObject(id)) { return id; } return users[id] || {id: id, deleted: true}; } function hasUser(id) { return angular.isObject(users[id]); } function getUserPhoto(id, placeholder) { var user = getUser(id); if (id == 333000) { return { placeholder: 'img/placeholders/DialogListAvatarSystem@2x.png' } }; return { placeholder: 'img/placeholders/' + placeholder + 'Avatar'+((Math.abs(id) % 8) + 1)+'@2x.png', location: user && user.photo && user.photo.photo_small }; } function getUserString (id) { var user = getUser(id); return 'u' + id + (user.access_hash ? '_' + user.access_hash : ''); } function getUserInput (id) { var user = getUser(id); if (user._ == 'userSelf') { return {_: 'inputUserSelf'}; } return { _: 'inputUserForeign', user_id: id, access_hash: user.access_hash || 0 }; } function wrapForFull (id) { var user = getUser(id); user.thumb = { placeholder: 'img/placeholders/UserAvatar'+((Math.abs(id) % 8) + 1)+'@2x.png', location: user && user.photo && user.photo.photo_small, width: 120, height: 120, size: 0 }; user.peerString = getUserString(id); return user; } function openUser (userID, accessHash) { var scope = $rootScope.$new(); scope.userID = userID; var modalInstance = $modal.open({ templateUrl: 'partials/user_modal.html', controller: 'UserModalController', scope: scope, windowClass: 'user_modal_window', resolve: { userFull: MtpApiManager.invokeApi('users.getFullUser', { id: getUserInput(userID) }).then(function (result) { saveApiUser(result.user); return result; }) } }); } $rootScope.openUser = openUser; $rootScope.$on('apiUpdate', function (e, update) { // dLog('on apiUpdate', update); switch (update._) { case 'updateUserStatus': var userID = update.user_id; if (users[userID]) { users[userID].status = update.status; $rootScope.$broadcast('user_update', userID); } break; case 'updateUserPhoto': var userID = update.user_id; if (users[userID]) { users[userID].photo = update.photo; $rootScope.$broadcast('user_update', userID); } break; } }); return { saveApiUsers: saveApiUsers, saveApiUser: saveApiUser, getUser: getUser, getUserPhoto: getUserPhoto, getUserString: getUserString, hasUser: hasUser, wrapForFull: wrapForFull, openUser: openUser } }) .service('AppChatsManager', function ($rootScope, $modal, MtpApiFileManager, MtpApiManager, AppUsersManager, RichTextProcessor) { var chats = {}; function saveApiChats (apiChats) { angular.forEach(apiChats, saveApiChat); }; function saveApiChat (apiChat) { if (!angular.isObject(apiChat)) { return; } apiChat.rTitle = RichTextProcessor.wrapRichText(apiChat.title, {noLinks: true, noLinebreaks: true}) || 'DELETED'; if (chats[apiChat.id] === undefined) { chats[apiChat.id] = apiChat; } else { angular.extend(chats[apiChat.id], apiChat); } }; function getChat (id) { return chats[id] || {id: id, deleted: true}; } function hasChat (id) { return angular.isObject(chats[id]); } function getChatPhoto(id, placeholder) { var chat = getChat(id); return { placeholder: 'img/placeholders/' + placeholder + 'Avatar'+((Math.abs(id) % 4) + 1)+'@2x.png', location: chat && chat.photo && chat.photo.photo_small }; } function getChatString (id) { var chat = getChat(id); return 'g' + id; } function wrapForFull (id, fullChat) { var chatFull = angular.copy(fullChat), chat = getChat(id); if (chatFull.participants && chatFull.participants._ == 'chatParticipants') { angular.forEach(chatFull.participants.participants, function(participant){ participant.user = AppUsersManager.getUser(participant.user_id); participant.userPhoto = AppUsersManager.getUserPhoto(participant.user_id, 'User'); participant.inviter = AppUsersManager.getUser(participant.inviter_id); }); } chatFull.thumb = { placeholder: 'img/placeholders/GroupAvatar'+((Math.abs(id) % 4) + 1)+'@2x.png', location: chat && chat.photo && chat.photo.photo_small, width: 120, height: 120, size: 0 }; chatFull.peerString = getChatString(id); chatFull.chat = chat; return chatFull; } function openChat (chatID, accessHash) { var scope = $rootScope.$new(); scope.chatID = chatID; var modalInstance = $modal.open({ templateUrl: 'partials/chat_modal.html?2', controller: 'ChatModalController', windowClass: 'chat_modal_window', scope: scope }); } $rootScope.openChat = openChat; return { saveApiChats: saveApiChats, saveApiChat: saveApiChat, getChat: getChat, getChatPhoto: getChatPhoto, getChatString: getChatString, hasChat: hasChat, wrapForFull: wrapForFull, openChat: openChat } }) .service('AppPeersManager', function (AppUsersManager, AppChatsManager) { return { getInputPeer: function (peerString) { var isUser = peerString.charAt(0) == 'u', peerParams = peerString.substr(1).split('_'); return isUser ? {_: 'inputPeerForeign', user_id: peerParams[0], access_hash: peerParams[1]} : {_: 'inputPeerChat', chat_id: peerParams[0]}; }, getInputPeerByID: function (peerID) { if (peerID > 0) { return { _: 'inputPeerForeign', user_id: peerID, access_hash: AppUsersManager.getUser(peerID).access_hash || 0 }; } else if (peerID < 0) { return { _: 'inputPeerChat', chat_id: -peerID }; } }, getOutputPeer: function (peerID) { return peerID > 0 ? {_: 'peerUser', user_id: peerID} : {_: 'peerChat', chat_id: -peerID}; }, getPeerID: function (peerString) { if (angular.isObject(peerString)) { return peerString.user_id ? peerString.user_id : -peerString.chat_id; } var isUser = peerString.charAt(0) == 'u', peerParams = peerString.substr(1).split('_'); return isUser ? peerParams[0] : -peerParams[0] || 0; }, getPeer: function (peerID) { return peerID > 0 ? AppUsersManager.getUser(peerID) : AppChatsManager.getChat(-peerID); }, getPeerPhoto: function (peerID, userPlaceholder, chatPlaceholder) { return peerID > 0 ? AppUsersManager.getUserPhoto(peerID, userPlaceholder) : AppChatsManager.getChatPhoto(-peerID, chatPlaceholder) } } }) .service('AppMessagesManager', function ($q, $rootScope, $filter, $sanitize, $location, ApiUpdatesManager, AppUsersManager, AppChatsManager, AppPeersManager, AppPhotosManager, AppVideoManager, AppDocsManager, MtpApiManager, MtpApiFileManager, RichTextProcessor, NotificationsManager) { var messagesStorage = {}; var messagesForHistory = {}; var historiesStorage = {}; var dialogsStorage = {count: null, dialogs: []}; var pendingByRandomID = {}; var pendingByMessageID = {}; var tempID = -1; NotificationsManager.start(); function getDialogs (offset, limit) { if (dialogsStorage.count !== null && dialogsStorage.dialogs.length >= offset + limit) { return $q.when({ count: dialogsStorage.count, dialogs: dialogsStorage.dialogs.slice(offset, offset + limit) }); } var deferred = $q.defer(); MtpApiManager.invokeApi('messages.getDialogs', { offset: offset, limit: limit, max_id: 0 }).then(function (dialogsResult) { AppUsersManager.saveApiUsers(dialogsResult.users); AppChatsManager.saveApiChats(dialogsResult.chats); saveMessages(dialogsResult.messages); dialogsStorage.count = dialogsResult._ == 'messages.dialogsSlice' ? dialogsResult.count : dialogsResult.dialogs.length; angular.forEach(dialogsResult.dialogs, function (dialog) { dialogsStorage.dialogs.push({ peerID: AppPeersManager.getPeerID(dialog.peer), top_message: dialog.top_message, unread_count: dialog.unread_count }); }); deferred.resolve({ count: dialogsStorage.count, dialogs: dialogsStorage.dialogs.slice(offset, offset + limit) }); }, function (error) { deferred.reject(error); }); return deferred.promise; } function getHistory (inputPeer, maxID, limit) { var peerID = AppPeersManager.getPeerID(inputPeer), historyStorage = historiesStorage[peerID], offset = 0, resultPending = []; if (historyStorage === undefined) { historyStorage = historiesStorage[peerID] = {count: null, history: [], pending: []}; } else if (!maxID && historyStorage.pending.length) { resultPending = historyStorage.pending.slice(); } if (maxID > 0) { for (offset = 0; offset < historyStorage.history.length; offset++) { if (maxID > historyStorage.history[offset]) { break; } } } // dLog('history storage', angular.copy(historyStorage.history), maxID, offset); if (historyStorage.count !== null && historyStorage.history.length >= offset + limit) { return $q.when({ count: historyStorage.count, history: resultPending.concat(historyStorage.history.slice(offset, offset + limit)) }); } var deferred = $q.defer(); MtpApiManager.invokeApi('messages.getHistory', { peer: inputPeer, offset: offset, limit: limit, max_id: maxID || 0 }).then(function (historyResult) { AppUsersManager.saveApiUsers(historyResult.users); AppChatsManager.saveApiChats(historyResult.chats); saveMessages(historyResult.messages); historyStorage.count = historyResult._ == 'messages.messagesSlice' ? historyResult.count : historyResult.messages.length; offset = 0; if (maxID > 0) { for (offset = 0; offset < historyStorage.history.length; offset++) { if (maxID > historyStorage.history[offset]) { break; } } } // dLog('history storage after', angular.copy(historyStorage.history), historyResult.messages, maxID, offset); historyStorage.history.splice(offset, historyStorage.history.length - offset); angular.forEach(historyResult.messages, function (message) { historyStorage.history.push(message.id); }); // dLog('history storage final', angular.copy(historyStorage.history), historyResult.messages, maxID, offset); deferred.resolve({ count: historyStorage.count, history: resultPending.concat(historyStorage.history.slice(offset, offset + limit)) }); }, function (error) { deferred.reject(error); }); return deferred.promise; } function processAffectedHistory (inputPeer, affectedHistory) { if (!ApiUpdatesManager.saveSeq(affectedHistory.seq)) { return false; } if (!affectedHistory.offset) { return $q.when(); } return MtpApiManager.invokeApi('messages.readHistory', { peer: inputPeer, offset: affectedHistory.offset, max_id: 0 }).then(function (affectedHistory) { return processAffectedHistory(inputPeer, affectedHistory); }); } function readHistory (inputPeer) { // dLog('start read'); var peerID = AppPeersManager.getPeerID(inputPeer), historyStorage = historiesStorage[peerID], foundDialog = getDialogByPeerID(peerID); if (!historyStorage || !historyStorage.history.length || foundDialog[0] && !foundDialog[0].unread_count) { // dLog('bad1', historyStorage, foundDialog[0]); return false; } var wasUnread = false; // dLog(historyStorage); for (i = 0; i < historyStorage.history.length; i++) { messageID = historyStorage.history[i]; message = messagesStorage[messageID]; // dLog('ms', message); if (message && !message.out) { if (message.unread) { // dLog('unread'); wasUnread = true; } else if (!wasUnread) { // dLog('bad2'); return false; } } } var promise = MtpApiManager.invokeApi('messages.readHistory', { peer: inputPeer, offset: 0, max_id: 0 }).then(function (affectedHistory) { return processAffectedHistory(inputPeer, affectedHistory); }).then(function () { if (foundDialog[0]) { foundDialog[0].unread_count = 0; $rootScope.$broadcast('dialog_unread', {peerID: peerID, count: 0}); } }); var messageID, message, i, peerID, foundDialog, dialog; for (i = 0; i < historyStorage.history.length; i++) { messageID = historyStorage.history[i]; message = messagesStorage[messageID]; if (message && !message.out) { message.unread = false; if (messagesForHistory[messageID]) { messagesForHistory[messageID].unread = false; } } } return promise; } function saveMessages (apiMessages) { angular.forEach(apiMessages, function (apiMessage) { messagesStorage[apiMessage.id] = apiMessage; if (apiMessage.media && apiMessage.media._ == 'messageMediaPhoto') { AppPhotosManager.savePhoto(apiMessage.media.photo); } if (apiMessage.media && apiMessage.media._ == 'messageMediaVideo') { AppVideoManager.saveVideo(apiMessage.media.video); } if (apiMessage.media && apiMessage.media._ == 'messageMediaDocument') { AppDocsManager.saveDoc(apiMessage.media.document); } if (apiMessage.action && apiMessage.action._ == 'messageActionChatEditPhoto') { AppPhotosManager.savePhoto(apiMessage.action.photo); } }); } 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, total: file.size} }; 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.done = progress.done; media.progress.percent = percent; if (historyMessage) { historyMessage.media.progress.done = progress.done; 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; if (toID < 0) { return toID; } else if (message.out) { return toID } return message.from_id; } function wrapForDialog (msgID, unreadCount) { var message = angular.copy(messagesStorage[msgID]) || {id: msgID}; message.fromUser = AppUsersManager.getUser(message.from_id); if (message.chatID = message.to_id.chat_id) { message.peerID = -message.chatID; message.peerData = AppChatsManager.getChat(message.chatID); message.peerString = AppChatsManager.getChatString(message.chatID); } else { message.peerID = message.out ? message.to_id.user_id : message.from_id; message.peerData = AppUsersManager.getUser(message.peerID); message.peerString = AppUsersManager.getUserString(message.peerID); } message.peerPhoto = AppPeersManager.getPeerPhoto(message.peerID, 'User', 'Group'); message.unreadCount = unreadCount; if (message._ == 'messageService' && message.action.user_id) { message.action.user = AppUsersManager.getUser(message.action.user_id); } if (message.message && message.message.length) { message.richMessage = RichTextProcessor.wrapRichText(message.message.substr(0, 64), {noLinks: true, noLinebreaks: true}); } return message; } function wrapForHistory (msgID) { if (messagesForHistory[msgID] !== undefined) { return messagesForHistory[msgID]; } var message = angular.copy(messagesStorage[msgID]) || {id: msgID}; message.fromUser = AppUsersManager.getUser(message.from_id); message.fromPhoto = AppUsersManager.getUserPhoto(message.from_id, 'User'); if (message._ == 'messageForwarded') { message.fwdUser = AppUsersManager.getUser(message.fwd_from_id); } if (message.media) { switch (message.media._) { case 'messageMediaPhoto': message.media.photo = AppPhotosManager.wrapForHistory(message.media.photo.id) break; 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) { message.media.user = AppUsersManager.getUser(message.media.user_id); message.media.userPhoto = AppUsersManager.getUserPhoto(message.media.user_id, 'User'); } } else if (message.action) { if (message.action._ == 'messageActionChatEditPhoto') { message.action.photo = AppPhotosManager.wrapForHistory(message.action.photo.id); } if (message.action.user_id) { message.action.user = AppUsersManager.getUser(message.action.user_id); message.action.userPhoto = AppUsersManager.getUserPhoto(message.action.user_id, 'User'); } } if (message.message && message.message.length) { message.richMessage = RichTextProcessor.wrapRichText(message.message); } return messagesForHistory[msgID] = message; } function getDialogByPeerID (peerID) { for (var i = 0; i < dialogsStorage.dialogs.length; i++) { if (dialogsStorage.dialogs[i].peerID == peerID) { return [dialogsStorage.dialogs[i], i]; } } return []; } function notifyAboutMessage (message) { var peerID = getMessagePeer(message); var fromUser = AppUsersManager.getUser(message.from_id); var fromPhoto = AppUsersManager.getUserPhoto(message.from_id, 'User'); var peerString; var notification = {}, notificationMessage = false, notificationPhoto; if (message.message) { notificationMessage = message.message; } else if (message.media && message.media._ != 'messageMediaEmpty') { switch (message.media._) { case 'messageMediaPhoto': notificationMessage = 'Photo'; break; case 'messageMediaVideo': notificationMessage = 'Video'; break; case 'messageMediaDocument': notificationMessage = 'Document'; break; case 'messageMediaGeo': notificationMessage = 'Location'; break; case 'messageMediaContact': notificationMessage = 'Contact'; break; default: notificationMessage = 'Attachment'; break; } } else if (message._ == 'messageService') { switch (message.action._) { case 'messageActionChatCreate': notificationMessage = 'created the group'; break; case 'messageActionChatEditTitle': notificationMessage = 'changed group name'; break; case 'messageActionChatEditPhoto': notificationMessage = 'changed group photo'; break; case 'messageActionChatDeletePhoto': notificationMessage = 'removed group photo'; break; case 'messageActionChatAddUser': notificationMessage = 'invited user'; break; case 'messageActionChatDeleteUser': notificationMessage = 'kicked user'; break; } } if (peerID > 0) { notification.title = (fromUser.first_name || '') + (fromUser.first_name && fromUser.last_name ? ' ' : '') + (fromUser.last_name || ''); notificationPhoto = fromPhoto; peerString = AppUsersManager.getUserString(peerID); } else { notification.title = (fromUser.first_name || fromUser.last_name || 'Somebody') + ' @ ' + (AppChatsManager.getChat(-peerID).title || 'Unknown chat'); notificationPhoto = AppChatsManager.getChatPhoto(-peerID, 'Chat'); peerString = AppChatsManager.getChatString(-peerID); } notification.onclick = function () { $location.url('/im?p=' + peerString); }; notification.message = notificationMessage; notification.image = notificationPhoto.placeholder; if (notificationPhoto.location) { MtpApiFileManager.downloadSmallFile(notificationPhoto.location, notificationPhoto.size).then(function (url) { notification.image = url; if (message.unread) { NotificationsManager.notify(notification); } }); } else { NotificationsManager.notify(notification); } } $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), historyStorage = historiesStorage[peerID]; if (historyStorage !== undefined) { var topMsgID = historiesStorage[peerID].history[0]; if (message.id <= topMsgID) { return false; } } else { historyStorage = historiesStorage[peerID] = {count: null, history: [], pending: []}; } saveMessages([message]); if (historyStorage.count !== null) { historyStorage.count++; } historyStorage.history.unshift(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; if (foundDialog.length) { dialog = foundDialog[0]; dialogsStorage.dialogs.splice(foundDialog[1], 1); } else { dialog = {peerID: peerID, unread_count: 0, top_message: false} } if (!message.out && message.unread) { dialog.unread_count++; } dialog.top_message = message.id; dialogsStorage.dialogs.unshift(dialog); $rootScope.$broadcast('dialogs_update', dialog); if ($rootScope.idle.isIDLE && !message.out && message.unread) { NotificationsManager.getPeerSettings(peerID).then(function (muted) { if (!message.unread || muted) { return; } notifyAboutMessage(message); }); } break; case 'updateReadMessages': var dialogsUpdated = {}, messageID, message, i, peerID, foundDialog, dialog; for (i = 0; i < update.messages.length; i++) { messageID = update.messages[i]; message = messagesStorage[messageID]; // dLog('read', messageID, message); if (message) { message.unread = false; if (messagesForHistory[messageID]) { // dLog(222, messagesForHistory[messageID]); messagesForHistory[messageID].unread = false; } peerID = getMessagePeer(message); if (!message.out) { foundDialog = getDialogByPeerID(peerID); if (foundDialog) { dialogsUpdated[peerID] = --foundDialog[0].unread_count; } } } } angular.forEach(dialogsUpdated, function(count, peerID) { $rootScope.$broadcast('dialog_unread', {peerID: peerID, count: count}); }); // $rootScope.$broadcast('history_update'); break; } }); return { getDialogs: getDialogs, getHistory: getHistory, readHistory: readHistory, saveMessages: saveMessages, sendText: sendText, sendFile: sendFile, getMessagePeer: getMessagePeer, wrapForDialog: wrapForDialog, wrapForHistory: wrapForHistory } }) .service('AppPhotosManager', function ($modal, $window, $rootScope, MtpApiFileManager, AppUsersManager) { var photos = {}; function savePhoto (apiPhoto) { photos[apiPhoto.id] = apiPhoto; angular.forEach(apiPhoto.sizes, function (photoSize) { if (photoSize._ == 'photoCachedSize') { MtpApiFileManager.saveSmallFile(photoSize.location, photoSize.bytes); // Memory photoSize.size = photoSize.bytes.length; delete photoSize.bytes; photoSize._ = 'photoSize'; } }); }; function choosePhotoSize (photo, width, height) { var bestPhotoSize = {_: 'photoSizeEmpty'}, bestDiff = 0xFFFFFF; angular.forEach(photo.sizes, function (photoSize) { var diff = Math.abs(photoSize.w * photoSize.h - width * height); if (diff < bestDiff) { bestPhotoSize = photoSize; bestDiff = diff; } }); return bestPhotoSize; } function wrapForHistory (photoID) { var photo = angular.copy(photos[photoID]) || {_: 'photoEmpty'}, width = 100, height = 100, thumbPhotoSize = choosePhotoSize(photo, width, height), thumb = { placeholder: 'img/placeholders/PhotoThumbConversation.gif', width: width, height: height }; // dLog('chosen photo size', photoID, thumbPhotoSize); if (thumbPhotoSize && thumbPhotoSize._ != 'photoSizeEmpty') { if (thumbPhotoSize.w > thumbPhotoSize.h) { thumb.height = parseInt(thumbPhotoSize.h * width / thumbPhotoSize.w); } else { thumb.width = parseInt(thumbPhotoSize.w * height / thumbPhotoSize.h); } thumb.location = thumbPhotoSize.location; thumb.size = thumbPhotoSize.size; } photo.thumb = thumb; return photo; } function wrapForFull (photoID) { var photo = wrapForHistory(photoID), fullWidth = 542, fullHeight = $($window).height() - 150, fullPhotoSize = choosePhotoSize(photo, fullWidth, fullHeight), full = { placeholder: 'img/placeholders/PhotoThumbModal.gif', width: fullWidth, height: fullHeight }; if (fullPhotoSize && fullPhotoSize._ != 'photoSizeEmpty') { if (fullPhotoSize.w > fullPhotoSize.h) { full.height = parseInt(fullPhotoSize.h * fullWidth / fullPhotoSize.w); } else { full.width = parseInt(fullPhotoSize.w * fullHeight / fullPhotoSize.h); if (full.width > fullWidth) { full.height = parseInt(full.height * fullWidth / full.width); full.width = fullWidth; } } full.location = fullPhotoSize.location; full.size = fullPhotoSize.size; } photo.full = full; photo.fromUser = AppUsersManager.getUser(photo.user_id); return photo; } function openPhoto (photoID, accessHash) { var scope = $rootScope.$new(true); scope.photoID = photoID; var modalInstance = $modal.open({ templateUrl: 'partials/photo_modal.html', controller: 'PhotoModalController', scope: scope, backdrop: 'static' }); } $rootScope.openPhoto = openPhoto; return { savePhoto: savePhoto, wrapForHistory: wrapForHistory, wrapForFull: wrapForFull, openPhoto: openPhoto } }) .service('AppVideoManager', function ($rootScope, $modal, $window, MtpApiFileManager, AppUsersManager) { var videos = {}; function saveVideo (apiVideo) { videos[apiVideo.id] = apiVideo; if (apiVideo.thumb && apiVideo.thumb._ == 'photoCachedSize') { MtpApiFileManager.saveSmallFile(apiVideo.thumb.location, apiVideo.thumb.bytes); // Memory apiVideo.thumb.size = apiVideo.thumb.bytes.length; delete apiVideo.thumb.bytes; apiVideo.thumb._ = 'photoSize'; } }; function wrapForHistory (videoID) { var video = angular.copy(videos[videoID]), width = 100, height = 100, thumbPhotoSize = video.thumb, thumb = { placeholder: 'img/placeholders/VideoThumbConversation.gif', width: width, height: height }; if (thumbPhotoSize && thumbPhotoSize._ != 'photoSizeEmpty') { if (thumbPhotoSize.w > thumbPhotoSize.h) { thumb.height = parseInt(thumbPhotoSize.h * width / thumbPhotoSize.w); } else { thumb.width = parseInt(thumbPhotoSize.w * height / thumbPhotoSize.h); } thumb.location = thumbPhotoSize.location; thumb.size = thumbPhotoSize.size; } video.thumb = thumb; return video; } function wrapForFull (videoID) { var video = wrapForHistory(videoID), fullWidth = 542, fullHeight = $($window).height() - 150, fullPhotoSize = video, full = { placeholder: 'img/placeholders/VideoThumbModal.gif', width: fullWidth, height: fullHeight, }; if (video.w > video.h) { full.height = parseInt(video.h * fullWidth / video.w); } else { full.width = parseInt(video.w * fullHeight / video.h); if (full.width > fullWidth) { full.height = parseInt(full.height * fullWidth / full.width); full.width = fullWidth; } } video.full = full; video.fromUser = AppUsersManager.getUser(video.user_id); return video; } function openVideo (videoID, accessHash) { var scope = $rootScope.$new(true); scope.videoID = videoID; scope.progress = {enabled: false}; scope.player = {}; // scope.close = function () { // modalInstance.close(); // } var modalInstance = $modal.open({ templateUrl: 'partials/video_modal.html', controller: 'VideoModalController', scope: scope }); } $rootScope.openVideo = openVideo; return { saveVideo: saveVideo, wrapForHistory: wrapForHistory, wrapForFull: wrapForFull, openVideo: openVideo } }) .service('AppDocsManager', function ($rootScope, $modal, $window, $timeout, MtpApiFileManager, AppUsersManager) { var docs = {}; var docsForHistory = {}; function saveDoc (apiDoc) { docs[apiDoc.id] = apiDoc; if (apiDoc.thumb && apiDoc.thumb._ == 'photoCachedSize') { MtpApiFileManager.saveSmallFile(apiDoc.thumb.location, apiDoc.thumb.bytes); // Memory apiDoc.thumb.size = apiDoc.thumb.bytes.length; delete apiDoc.thumb.bytes; apiDoc.thumb._ = 'photoSize'; } }; function wrapForHistory (docID) { if (docsForHistory[docID] !== undefined) { return docsForHistory[docID]; } var doc = angular.copy(docs[docID]), width = 100, height = 100, thumbPhotoSize = doc.thumb, thumb = { placeholder: 'img/placeholders/DocThumbConversation.jpg', width: width, height: height }; if (thumbPhotoSize && thumbPhotoSize._ != 'photoSizeEmpty') { if (thumbPhotoSize.w > thumbPhotoSize.h) { thumb.height = parseInt(thumbPhotoSize.h * width / thumbPhotoSize.w); } else { thumb.width = parseInt(thumbPhotoSize.w * height / thumbPhotoSize.h); } thumb.location = thumbPhotoSize.location; thumb.size = thumbPhotoSize.size; } else { thumb = false; } doc.thumb = thumb; 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 }; historyDoc.progress = {enabled: true, percent: 1, total: doc.size}; function updateDownloadProgress (progress) { dLog('dl progress', progress); historyDoc.progress.done = progress.done; 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) { var ext = (doc.file_name.split('.', 2) || [])[1] || '', mime = doc.mime_type; chrome.fileSystem.chooseEntry({ type: 'saveFile', suggestedName: doc.file_name, accepts: [{ mimeTypes: [mime], extensions: [ext] }] }, 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) { 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(':'); a[0].click(); $timeout(function () { a.remove(); }, 100); }, function (e) { dLog('document download failed', e); historyDoc.progress.enabled = false; }, updateDownloadProgress); } } $rootScope.openDoc = openDoc; return { saveDoc: saveDoc, wrapForHistory: wrapForHistory, openDoc: openDoc } }) .service('ExternalResourcesManager', function ($q, $http) { var urlPromises = {}; function downloadImage (url) { if (urlPromises[url] !== undefined) { return urlPromises[url]; } var deferred = $q.defer(); $http.get(url, {responseType: 'blob', transformRequest: null}) .then( function (response) { deferred.resolve(window.webkitURL.createObjectURL(response.data)); }, function (error) { deferred.reject(error); } ); return urlPromises[url] = deferred.promise; } return { downloadImage: downloadImage } }) .service('ApiUpdatesManager', function ($rootScope, MtpNetworkerFactory, AppUsersManager, AppChatsManager, AppPeersManager, MtpApiManager) { var curState = {invalid: true}; function processUpdateMessage (updateMessage) { if (curState.invalid) { return false; } if (updateMessage.seq) { if (!saveSeq(updateMessage.seq, updateMessage.seq_start)) { return false; } if (updateMessage.date) { curState.date = updateMessage.date; } } switch (updateMessage._) { case 'updatesTooLong': getDifference(); break; case 'updateShort': saveUpdate(updateMessage.update); break; case 'updatesCombined': case 'updates': AppUsersManager.saveApiUsers(updateMessage.users); AppChatsManager.saveApiChats(updateMessage.chats); var i, update, message; for (var i = 0; i < updateMessage.updates.length; i++) { update = updateMessage.updates[i]; switch (update._) { case 'updateNewMessage': message = update.message; if (message.from_id && !AppUsersManager.hasUser(message.from_id)) { dLog('User not found', message.from_id, 'getDiff'); getDifference(); return false; } if (message.to_id.chat_id && !AppChatsManager.hasChat(message.to_id.chat_id)) { dLog('Chat not found', message.to_id.chat_id, 'getDiff'); getDifference(); return false; } break; } } angular.forEach(updateMessage.updates, function (update) { saveUpdate(update); }); break; case 'updateShortMessage': if (!AppUsersManager.hasUser(updateMessage.from_id)) { dLog('User not found', updateMessage.from_id, 'getDiff'); getDifference(); break; } saveUpdate({ _: 'updateNewMessage', message: { _: 'message', id: updateMessage.id, from_id: updateMessage.from_id, to_id: AppPeersManager.getOutputPeer(MtpApiManager.getUserID()), out: false, unread: true, date: updateMessage.date, message: updateMessage.message, media: {_: 'messageMediaEmpty'} }, pts: updateMessage.pts }); break; case 'updateShortChatMessage': if (!AppUsersManager.hasUser(updateMessage.from_id) || !AppChatsManager.hasChat(updateMessage.chat_id)) { dLog('User or chat not found', updateMessage.from_id, updateMessage.chat_id, 'getDiff'); getDifference(); break; } saveUpdate({ _: 'updateNewMessage', message: { _: 'message', id: updateMessage.id, from_id: updateMessage.from_id, to_id: AppPeersManager.getOutputPeer(-updateMessage.chat_id), out: false, unread: true, date: updateMessage.date, message: updateMessage.message, media: {_: 'messageMediaEmpty'} }, pts: updateMessage.pts }); break; } return true; } function getDifference (force) { if (curState.invalid && !force) { return false; } curState.invalid = true; MtpApiManager.invokeApi('updates.getDifference', {pts: curState.pts, date: curState.date, qts: 0}).then(function (differenceResult) { if (differenceResult._ == 'updates.differenceEmpty') { curState.date = differenceResult.date; curState.seq = differenceResult.seq; delete curState.invalid; return false; } 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', message: apiMessage, pts: curState.pts }, true); }); var nextState = differenceResult.intermediate_state || differenceResult.state; curState.seq = nextState.seq; curState.pts = nextState.pts; curState.date = nextState.date; if (differenceResult._ == 'updates.differenceSlice') { getDifference(true); } else { delete curState.invalid; } }); } function saveUpdate (update, force) { if (curState.invalid && !force) { return false; } if (update.pts) { curState.pts = update.pts; } $rootScope.$broadcast('apiUpdate', update); } function saveSeq (seq, seqStart) { // dLog('saving seq', curState.invalid, seq, seqStart, curState.seq); if (curState.invalid) { return false; } seqStart = seqStart || seq; if (seqStart != curState.seq + 1) { // dLog('seq hole', seqStart, curState.seq); if (seqStart != curState.seq) { getDifference(); } return false; } curState.seq = seq; return true; } function attach () { MtpNetworkerFactory.setUpdatesProcessor(processUpdateMessage); MtpApiManager.invokeApi('updates.getState').then(function (stateResult) { curState.seq = stateResult.seq; curState.pts = stateResult.pts; curState.date = stateResult.date; delete curState.invalid; }) } return { saveUpdate: saveUpdate, saveSeq: saveSeq, attach: attach } }) .service('RichTextProcessor', function ($sce, $sanitize) { var emojiUtf = [], emojiMap = {}, emojiData = Config.Emoji, emojiCode; for (emojiCode in emojiData) { emojiUtf.push(emojiData[emojiCode][0]); emojiMap[emojiData[emojiCode][0]] = emojiCode; } var regExp = new RegExp('((?:(ftp|https?)://|(?:mailto:)?([A-Za-z0-9._%+-]+@))(\\S*\\.\\S*[^\\s.;,(){}<>"\']))|(\\n)|(' + emojiUtf.join('|') + ')', 'i'); // dLog(regExp); return { wrapRichText: wrapRichText }; function encodeEntities(value) { return value. replace(/&/g, '&'). replace(/([^\#-~| |!])/g, function (value) { // non-alphanumeric return '&#' + value.charCodeAt(0) + ';'; }). replace(//g, '>'); }; function wrapRichText(text, options) { if (!text || !text.length) { return ''; } options = options || {}; text = text.replace(/\ufe0f/g, '', text); var match, raw = text, html = [], url, emojiTitle, emojiFound = false; while ((match = raw.match(regExp))) { // dLog(2, match); html.push(encodeEntities(raw.substr(0, match.index))); if (match[1]) { // URL if (!options.noLinks) { if (match[3]) { html.push( '', encodeEntities(match[3] + match[4]), '' ); } else { html.push( '', encodeEntities((match[2] != 'http' ? match[2] + '://' : '') + match[4]), '' ); } } else { html.push(encodeEntities(match[0])); } } else if (match[5]) { // New line if (!options.noLinebreaks) { html.push('
'); } else { html.push(' '); } } else if (match[6]) { if (emojiCode = emojiMap[match[6]]) { emojiFound = true; emojiTitle = encodeEntities(emojiData[emojiCode][1][0]); html.push( ':', emojiTitle, ':' ); } else { html.push(encodeEntities(match[6])); } } raw = raw.substr(match.index + match[0].length); } html.push(encodeEntities(raw)); text = $sanitize(html.join('')); // dLog(3, text, html); if (emojiFound) { text = text.replace(//g, ''); } // dLog(4, text, html); return $sce.trustAs('html', text); } }) .service('IdleManager', function ($rootScope, $window, $timeout) { $rootScope.idle = {isIDLE: false}; var toPromise; return { start: start }; function start () { $($window).on('blur focus keydown mousedown touchstart', onEvent); } function onEvent (e) { // dLog('event', e.type); if (e.type == 'mousemove') { $($window).off('mousemove', onEvent); } var isIDLE = e.type == 'blur' || e.type == 'timeout' ? true : false; $timeout.cancel(toPromise); if (!isIDLE) { // dLog('update timeout'); toPromise = $timeout(function () { onEvent({type: 'timeout'}); }, 30000); } if ($rootScope.idle.isIDLE == isIDLE) { return; } // dLog('IDLE changed', isIDLE); $rootScope.$apply(function () { $rootScope.idle.isIDLE = isIDLE; }); if (isIDLE && e.type == 'timeout') { $($window).on('mousemove', onEvent); } } }) .service('NotificationsManager', function ($rootScope, $window, $timeout, $interval, MtpApiManager, AppPeersManager, IdleManager) { var notificationsUiSupport = window.webkitNotifications !== undefined; var notificationsShown = []; var notificationsCount = 0; var peerSettings = {}; var faviconEl = $('link[rel="icon"]'); var titleBackup = document.title, faviconBackup = faviconEl.attr('href'), titlePromise; $rootScope.$watch('idle.isIDLE', function (newVal) { // dLog('isIDLE watch', newVal); $interval.cancel(titlePromise); if (!newVal) { notificationsCount = 0; document.title = titleBackup; faviconEl.attr('href', faviconBackup); notificationsClear(); } else { titleBackup = document.title; titlePromise = $interval(function () { var time = +new Date(); // dLog('check title', notificationsCount, time % 2000 > 1000); if (!notificationsCount || time % 2000 > 1000) { document.title = titleBackup; faviconEl.attr('href', faviconBackup); } else { document.title = notificationsCount + ' notifications'; faviconEl.attr('href', 'favicon_unread.ico'); } }, 1000); } }); return { start: start, notify: notify, getPeerSettings: getPeerSettings }; function getPeerSettings (peerID) { if (peerSettings[peerID] !== undefined) { return peerSettings[peerID]; } return peerSettings[peerID] = MtpApiManager.invokeApi('account.getNotifySettings', { peer: { _: 'inputNotifyPeer', peer: AppPeersManager.getInputPeerByID(peerID) } }).then(function (peerNotifySettings) { // dLog('got settings', peerID, peerNotifySettings); return peerNotifySettings._ == 'peerNotifySettings' && peerNotifySettings.mute_until * 1000 > (+new Date()); }); } function start () { if (!notificationsUiSupport) { return false; } var havePermission = window.webkitNotifications.checkPermission(); // dLog('perm', havePermission); if (havePermission != 0) { // 0 is PERMISSION_ALLOWED $($window).on('click', requestPermission); } $($window).on('beforeunload', notificationsClear); } function requestPermission() { window.webkitNotifications.requestPermission(); $($window).off('click', requestPermission); } function notify (data) { // dLog('notify', $rootScope.idle.isIDLE); if (!$rootScope.idle.isIDLE) { return false; } notificationsCount++; if (!notificationsUiSupport || window.webkitNotifications.checkPermission() != 0) { return false; } var notification = window.webkitNotifications.createNotification( data.image || '', data.title || '', data.message || '' ); notification.onclick = function () { notification.close(); window.focus(); notificationsClear(); if (data.onclick) { data.onclick(); } }; // dLog('notify', notification); notification.show(); notificationsShown.push(notification); }; function notificationsClear() { angular.forEach(notificationsShown, function (notification) { try { if (notification.close) { notification.close() } else if (notification.cancel) { notification.cancel(); } } catch (e) {} }); notificationsShown = []; } }) .service('ErrorService', function ($rootScope, $modal) { function showError (templateUrl, params, options) { var scope = $rootScope.$new(); angular.extend(scope, params); return $modal.open({ templateUrl: templateUrl, // controller: 'ErrorModalController', scope: scope, windowClass: options.windowClass || '' }); } function showSimpleError (title, description) { return showError ('partials/error_modal.html', { title: title, description: description }, { windowClass: 'error_modal_window' }); }; return { showError: showError, showSimpleError: showSimpleError } })