/*! * Webogram v0.7 - 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', ['myApp.i18n', 'izhukov.utils']) .service('AppUsersManager', function ($rootScope, $modal, $modalStack, $filter, $q, qSync, MtpApiManager, RichTextProcessor, ServerTimeManager, Storage, _) { var users = {} var usernames = {} var userAccess = {} var cachedPhotoLocations = {} var contactsIndex = SearchIndexManager.createIndex() var contactsFillPromise var contactsList var myID MtpApiManager.getUserID().then(function (id) { myID = id }) function fillContacts () { if (contactsFillPromise) { return contactsFillPromise } return contactsFillPromise = MtpApiManager.invokeApi('contacts.getContacts', { hash: 0 }).then(function (result) { var userID, searchText var i contactsList = [] saveApiUsers(result.users) for (var i = 0; i < result.contacts.length; i++) { userID = result.contacts[i].user_id contactsList.push(userID) SearchIndexManager.indexObject(userID, getUserSearchText(userID), contactsIndex) } return contactsList }) } function getUserSearchText (id) { var user = users[id] if (!user) { return false } var serviceText = '' if (user.pFlags.self) { serviceText = _('user_name_saved_msgs_raw') } return (user.first_name || '') + ' ' + (user.last_name || '') + ' ' + (user.phone || '') + ' ' + (user.username || '') + ' ' + serviceText } function getContacts (query) { return fillContacts().then(function (contactsList) { if (angular.isString(query) && query.length) { var results = SearchIndexManager.search(query, contactsIndex) var filteredContactsList = [] for (var i = 0; i < contactsList.length; i++) { if (results[contactsList[i]]) { filteredContactsList.push(contactsList[i]) } } contactsList = filteredContactsList } contactsList.sort(function (userID1, userID2) { var sortName1 = (users[userID1] || {}.sortName) || '' var sortName2 = (users[userID2] || {}.sortName) || '' if (sortName1 == sortName2) { return 0 } return sortName1 > sortName2 ? 1 : -1 }) return contactsList }) } function resolveUsername (username) { return usernames[username] || 0 } function saveApiUsers (apiUsers) { angular.forEach(apiUsers, saveApiUser) } function saveApiUser (apiUser, noReplace) { if (!angular.isObject(apiUser) || noReplace && angular.isObject(users[apiUser.id]) && users[apiUser.id].first_name) { return } var userID = apiUser.id var result = users[userID] if (apiUser.pFlags === undefined) { apiUser.pFlags = {} } if (apiUser.pFlags.min) { if (result !== undefined) { return } } if (apiUser.phone) { apiUser.rPhone = $filter('phoneNumber')(apiUser.phone) } apiUser.num = (Math.abs(userID) % 8) + 1 if (apiUser.first_name) { apiUser.rFirstName = RichTextProcessor.wrapRichText(apiUser.first_name, {noLinks: true, noLinebreaks: true}) apiUser.rFullName = apiUser.last_name ? RichTextProcessor.wrapRichText(apiUser.first_name + ' ' + (apiUser.last_name || ''), {noLinks: true, noLinebreaks: true}) : apiUser.rFirstName } else { apiUser.rFirstName = RichTextProcessor.wrapRichText(apiUser.last_name, {noLinks: true, noLinebreaks: true}) || apiUser.rPhone || _('user_first_name_deleted') apiUser.rFullName = RichTextProcessor.wrapRichText(apiUser.last_name, {noLinks: true, noLinebreaks: true}) || apiUser.rPhone || _('user_name_deleted') } if (apiUser.username) { var searchUsername = SearchIndexManager.cleanUsername(apiUser.username) usernames[searchUsername] = userID } apiUser.sortName = apiUser.pFlags.deleted ? '' : SearchIndexManager.cleanSearchText(apiUser.first_name + ' ' + (apiUser.last_name || '')) var nameWords = apiUser.sortName.split(' ') var firstWord = nameWords.shift() var lastWord = nameWords.pop() apiUser.initials = firstWord.charAt(0) + (lastWord ? lastWord.charAt(0) : firstWord.charAt(1)) if (apiUser.status) { if (apiUser.status.expires) { apiUser.status.expires -= ServerTimeManager.serverTimeOffset } if (apiUser.status.was_online) { apiUser.status.was_online -= ServerTimeManager.serverTimeOffset } } if (apiUser.pFlags.bot) { apiUser.sortStatus = -1 } else { apiUser.sortStatus = getUserStatusForSort(apiUser.status) } var result = users[userID] if (result === undefined) { result = users[userID] = apiUser } else { safeReplaceObject(result, apiUser) } $rootScope.$broadcast('user_update', userID) if (cachedPhotoLocations[userID] !== undefined) { safeReplaceObject(cachedPhotoLocations[userID], apiUser && apiUser.photo && apiUser.photo.photo_small || {empty: true}) } } function saveUserAccess (id, accessHash) { userAccess[id] = accessHash } function getUserStatusForSort (status) { if (status) { var expires = status.expires || status.was_online if (expires) { return expires } var timeNow = tsNow(true) switch (status._) { case 'userStatusRecently': return timeNow - 86400 * 3 case 'userStatusLastWeek': return timeNow - 86400 * 7 case 'userStatusLastMonth': return timeNow - 86400 * 30 } } return 0 } function getUser (id) { if (angular.isObject(id)) { return id } return users[id] || {id: id, deleted: true, num: 1, access_hash: userAccess[id]} } function getSelf () { return getUser(myID) } function isBot (id) { return users[id] && users[id].pFlags.bot } function hasUser (id, allowMin) { var user = users[id] return angular.isObject(user) && (allowMin || !user.pFlags.min) } function getUserPhoto (id) { var user = getUser(id) if (id == 333000) { return { placeholder: 'img/placeholders/DialogListAvatarSystem@2x.png' } } if (cachedPhotoLocations[id] === undefined) { cachedPhotoLocations[id] = user && user.photo && user.photo.photo_small || {empty: true} } return { num: user.num, placeholder: 'img/placeholders/UserAvatar' + user.num + '@2x.png', location: cachedPhotoLocations[id] } } 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.pFlags.self) { return {_: 'inputUserSelf'} } return { _: 'inputUser', user_id: id, access_hash: user.access_hash || 0 } } function updateUsersStatuses () { var timestampNow = tsNow(true) angular.forEach(users, function (user) { if (user.status && user.status._ == 'userStatusOnline' && user.status.expires < timestampNow) { user.status = user.status.wasStatus || {_: 'userStatusOffline', was_online: user.status.expires} delete user.status.wasStatus $rootScope.$broadcast('user_update', user.id) } }) } function forceUserOnline (id) { if (isBot(id)) { return } var user = getUser(id) if (user && user.status && user.status._ != 'userStatusOnline' && user.status._ != 'userStatusEmpty') { var wasStatus if (user.status._ != 'userStatusOffline') { delete user.status.wasStatus wasStatus = angular.copy(user.status) } user.status = { _: 'userStatusOnline', expires: tsNow(true) + 60, wasStatus: wasStatus } user.sortStatus = getUserStatusForSort(user.status) $rootScope.$broadcast('user_update', id) } } function wrapForFull (id) { var user = getUser(id) return user } function openUser (userID, override) { var scope = $rootScope.$new() scope.userID = userID scope.override = override || {} var modalInstance = $modal.open({ templateUrl: templateUrl('user_modal'), controller: 'UserModalController', scope: scope, windowClass: 'user_modal_window mobile_modal', backdrop: 'single' }) } function importContact (phone, firstName, lastName) { return MtpApiManager.invokeApi('contacts.importContacts', { contacts: [{ _: 'inputPhoneContact', client_id: '1', phone: phone, first_name: firstName, last_name: lastName }], replace: false }).then(function (importedContactsResult) { saveApiUsers(importedContactsResult.users) var foundUserID = false angular.forEach(importedContactsResult.imported, function (importedContact) { onContactUpdated(foundUserID = importedContact.user_id, true) }) return foundUserID || false }) } function importContacts (contacts) { var inputContacts = [], i var j for (i = 0; i < contacts.length; i++) { for (j = 0; j < contacts[i].phones.length; j++) { inputContacts.push({ _: 'inputPhoneContact', client_id: (i << 16 | j).toString(10), phone: contacts[i].phones[j], first_name: contacts[i].first_name, last_name: contacts[i].last_name }) } } return MtpApiManager.invokeApi('contacts.importContacts', { contacts: inputContacts, replace: false }).then(function (importedContactsResult) { saveApiUsers(importedContactsResult.users) var result = [] angular.forEach(importedContactsResult.imported, function (importedContact) { onContactUpdated(importedContact.user_id, true) result.push(importedContact.user_id) }) return result }) } function deleteContacts (userIDs) { var ids = [] angular.forEach(userIDs, function (userID) { ids.push(getUserInput(userID)) }) return MtpApiManager.invokeApi('contacts.deleteContacts', { id: ids }).then(function () { angular.forEach(userIDs, function (userID) { onContactUpdated(userID, false) }) }) } function onContactUpdated (userID, isContact) { userID = parseInt(userID) if (angular.isArray(contactsList)) { var curPos = curIsContact = contactsList.indexOf(userID) var curIsContact = curPos != -1 if (isContact != curIsContact) { if (isContact) { contactsList.push(userID) SearchIndexManager.indexObject(userID, getUserSearchText(userID), contactsIndex) } else { contactsList.splice(curPos, 1) } $rootScope.$broadcast('contacts_update', userID) } } } function openImportContact () { return $modal.open({ templateUrl: templateUrl('import_contact_modal'), controller: 'ImportContactModalController', windowClass: 'md_simple_modal_window mobile_modal' }).result.then(function (foundUserID) { if (!foundUserID) { return $q.reject() } return foundUserID }) } function setUserStatus (userID, offline) { if (isBot(userID)) { return } var user = users[userID] if (user) { var status = offline ? { _: 'userStatusOffline', was_online: tsNow(true) } : { _: 'userStatusOnline', expires: tsNow(true) + 500 } user.status = status user.sortStatus = getUserStatusForSort(user.status) $rootScope.$broadcast('user_update', userID) } } $rootScope.$on('apiUpdate', function (e, update) { // console.log('on apiUpdate', update) switch (update._) { case 'updateUserStatus': var userID = update.user_id var user = users[userID] if (user) { user.status = update.status if (user.status) { if (user.status.expires) { user.status.expires -= ServerTimeManager.serverTimeOffset } if (user.status.was_online) { user.status.was_online -= ServerTimeManager.serverTimeOffset } } user.sortStatus = getUserStatusForSort(user.status) $rootScope.$broadcast('user_update', userID) } break case 'updateUserPhoto': var userID = update.user_id var user = users[userID] if (user) { forceUserOnline(userID) if (!user.photo) { user.photo = update.photo } else { safeReplaceObject(user.photo, update.photo) } if (cachedPhotoLocations[userID] !== undefined) { safeReplaceObject(cachedPhotoLocations[userID], update.photo && update.photo.photo_small || {empty: true}) } $rootScope.$broadcast('user_update', userID) } break case 'updateContactLink': onContactUpdated(update.user_id, update.my_link._ == 'contactLinkContact') break } }) $rootScope.$on('user_auth', function (e, userAuth) { myID = userAuth && userAuth.id || 0 }) setInterval(updateUsersStatuses, 60000) $rootScope.$on('stateSynchronized', updateUsersStatuses) return { getContacts: getContacts, saveApiUsers: saveApiUsers, saveApiUser: saveApiUser, saveUserAccess: saveUserAccess, getUser: getUser, getSelf: getSelf, getUserInput: getUserInput, setUserStatus: setUserStatus, forceUserOnline: forceUserOnline, getUserPhoto: getUserPhoto, getUserString: getUserString, getUserSearchText: getUserSearchText, hasUser: hasUser, isBot: isBot, importContact: importContact, importContacts: importContacts, deleteContacts: deleteContacts, wrapForFull: wrapForFull, openUser: openUser, resolveUsername: resolveUsername, openImportContact: openImportContact } }) .service('PhonebookContactsService', function ($q, $modal, $sce, FileManager) { return { isAvailable: isAvailable, openPhonebookImport: openPhonebookImport, getPhonebookContacts: getPhonebookContacts } function isAvailable () { if (Config.Mobile && Config.Navigator.ffos && Config.Modes.packed) { try { return navigator.mozContacts && navigator.mozContacts.getAll } catch (e) { console.error(dT(), 'phonebook n/a', e) return false } } return false } function openPhonebookImport () { return $modal.open({ templateUrl: templateUrl('phonebook_modal'), controller: 'PhonebookModalController', windowClass: 'phonebook_modal_window mobile_modal' }) } function getPhonebookContacts () { try { var request = window.navigator.mozContacts.getAll({}) } catch (e) { return $q.reject(e) } var deferred = $q.defer() var contacts = [] var count = 0 request.onsuccess = function () { if (this.result) { var contact = { id: count, first_name: (this.result.givenName || []).join(' '), last_name: (this.result.familyName || []).join(' '), phones: [] } if (this.result.tel != undefined) { for (var i = 0; i < this.result.tel.length; i++) { contact.phones.push(this.result.tel[i].value) } } if (this.result.photo && this.result.photo[0]) { try { contact.photo = FileManager.getUrl(this.result.photo[0]) } catch (e) {} } if (!contact.photo) { contact.photo = 'img/placeholders/UserAvatar' + ((Math.abs(count) % 8) + 1) + '@2x.png' } contact.photo = $sce.trustAsResourceUrl(contact.photo) count++ contacts.push(contact) } if (!this.result || count >= 1000) { deferred.resolve(contacts) return } this['continue']() } request.onerror = function (e) { console.log('phonebook error', e, e.type, e.message) deferred.reject(e) } return deferred.promise } }) .service('AppChatsManager', function ($q, $rootScope, $modal, _, MtpApiManager, AppUsersManager, AppPhotosManager, RichTextProcessor) { var chats = {} var usernames = {} var channelAccess = {} var megagroups = {} var cachedPhotoLocations = {} 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}) || _('chat_title_deleted') var result = chats[apiChat.id] var titleWords = SearchIndexManager.cleanSearchText(apiChat.title || '').split(' ') var firstWord = titleWords.shift() var lastWord = titleWords.pop() apiChat.initials = firstWord.charAt(0) + (lastWord ? lastWord.charAt(0) : firstWord.charAt(1)) apiChat.num = (Math.abs(apiChat.id >> 1) % 8) + 1 if (apiChat.pFlags === undefined) { apiChat.pFlags = {} } if (apiChat.pFlags.min) { if (result !== undefined) { return } } if (apiChat._ == 'channel' && apiChat.participants_count === undefined && result !== undefined && result.participants_count) { apiChat.participants_count = result.participants_count } if (apiChat.username) { var searchUsername = SearchIndexManager.cleanUsername(apiChat.username) usernames[searchUsername] = apiChat.id } if (result === undefined) { result = chats[apiChat.id] = apiChat } else { safeReplaceObject(result, apiChat) $rootScope.$broadcast('chat_update', apiChat.id) } if (cachedPhotoLocations[apiChat.id] !== undefined) { safeReplaceObject(cachedPhotoLocations[apiChat.id], apiChat && apiChat.photo && apiChat.photo.photo_small || {empty: true}) } } function getChat (id) { return chats[id] || {id: id, deleted: true, access_hash: channelAccess[id]} } function hasRights (id, action) { if (chats[id] === undefined) { return false } var chat = getChat(id) if (chat._ == 'chatForbidden' || chat._ == 'channelForbidden' || chat.pFlags.kicked || chat.pFlags.left) { return false } if (chat.pFlags.creator) { return true } switch (action) { case 'send': if (chat._ == 'channel' && !chat.pFlags.megagroup && !chat.pFlags.editor) { return false } break case 'edit_title': case 'edit_photo': case 'invite': if (chat._ == 'channel') { if (chat.pFlags.megagroup) { if (!chat.pFlags.editor && !(action == 'invite' && chat.pFlags.democracy)) { return false } } else { return false } } else { if (chat.pFlags.admins_enabled && !chat.pFlags.admin) { return false } } break } return true } function resolveUsername (username) { return usernames[username] || 0 } function saveChannelAccess (id, accessHash) { channelAccess[id] = accessHash } function saveIsMegagroup (id) { megagroups[id] = true } function isChannel (id) { var chat = chats[id] if (chat && (chat._ == 'channel' || chat._ == 'channelForbidden') || channelAccess[id]) { return true } return false } function isMegagroup (id) { if (megagroups[id]) { return true } var chat = chats[id] if (chat && chat._ == 'channel' && chat.pFlags.megagroup) { return true } return false } function isBroadcast (id) { return isChannel(id) && !isMegagroup(id) } function getChatInput (id) { return id || 0 } function getChannelInput (id) { if (!id) { return {_: 'inputChannelEmpty'} } return { _: 'inputChannel', channel_id: id, access_hash: getChat(id).access_hash || channelAccess[id] || 0 } } function hasChat (id, allowMin) { var chat = chats[id] return angular.isObject(chat) && (allowMin || !chat.pFlags.min) } function getChatPhoto (id) { var chat = getChat(id) if (cachedPhotoLocations[id] === undefined) { cachedPhotoLocations[id] = chat && chat.photo && chat.photo.photo_small || {empty: true} } return { placeholder: 'img/placeholders/GroupAvatar' + Math.ceil(chat.num / 2) + '@2x.png', location: cachedPhotoLocations[id] } } function getChatString (id) { var chat = getChat(id) if (isChannel(id)) { return (isMegagroup(id) ? 's' : 'c') + id + '_' + chat.access_hash } return 'g' + id } function wrapForFull (id, fullChat) { var chatFull = angular.copy(fullChat) var chat = getChat(id) if (!chatFull.participants_count) { chatFull.participants_count = chat.participants_count } if (chatFull.participants && chatFull.participants._ == 'chatParticipants') { chatFull.participants.participants = wrapParticipants(id, chatFull.participants.participants) } if (chatFull.about) { chatFull.rAbout = RichTextProcessor.wrapRichText(chatFull.about, {noLinebreaks: true}) } chatFull.peerString = getChatString(id) chatFull.chat = chat return chatFull } function wrapParticipants(id, participants) { var chat = getChat(id) if (isChannel(id)) { var isAdmin = chat.pFlags.creator || chat.pFlags.editor || chat.pFlags.moderator angular.forEach(participants, function (participant) { participant.canLeave = participant._ == 'channelParticipantSelf' participant.canKick = isAdmin && participant._ == 'channelParticipant' // just for order by last seen participant.user = AppUsersManager.getUser(participant.user_id) }) } else { var myID = AppUsersManager.getSelf().id var isAdmin = chat.pFlags.creator || chat.pFlags.admins_enabled && chat.pFlags.admin angular.forEach(participants, function (participant) { participant.canLeave = myID == participant.user_id participant.canKick = !participant.canLeave && ( chat.pFlags.creator || participant._ == 'chatParticipant' && (isAdmin || myID == participant.inviter_id) ) // just for order by last seen participant.user = AppUsersManager.getUser(participant.user_id) }) } return participants } function openChat (chatID, accessHash) { var scope = $rootScope.$new() scope.chatID = chatID if (isChannel(chatID)) { var modalInstance = $modal.open({ templateUrl: templateUrl('channel_modal'), controller: 'ChannelModalController', scope: scope, windowClass: 'chat_modal_window channel_modal_window mobile_modal' }) } else { var modalInstance = $modal.open({ templateUrl: templateUrl('chat_modal'), controller: 'ChatModalController', scope: scope, windowClass: 'chat_modal_window mobile_modal' }) } } $rootScope.$on('apiUpdate', function (e, update) { // console.log('on apiUpdate', update) switch (update._) { case 'updateChannel': var channelID = update.channel_id $rootScope.$broadcast('channel_settings', {channelID: channelID}) break } }) return { saveApiChats: saveApiChats, saveApiChat: saveApiChat, getChat: getChat, isChannel: isChannel, isMegagroup: isMegagroup, isBroadcast: isBroadcast, hasRights: hasRights, saveChannelAccess: saveChannelAccess, saveIsMegagroup: saveIsMegagroup, getChatInput: getChatInput, getChannelInput: getChannelInput, getChatPhoto: getChatPhoto, getChatString: getChatString, resolveUsername: resolveUsername, hasChat: hasChat, wrapForFull: wrapForFull, wrapParticipants: wrapParticipants, openChat: openChat } }) .service('AppPeersManager', function ($q, qSync, AppUsersManager, AppChatsManager, MtpApiManager) { function getInputPeer (peerString) { var firstChar = peerString.charAt(0) var peerParams = peerString.substr(1).split('_') if (firstChar == 'u') { AppUsersManager.saveUserAccess(peerParams[0], peerParams[1]) return { _: 'inputPeerUser', user_id: peerParams[0], access_hash: peerParams[1] } } else if (firstChar == 'c' || firstChar == 's') { AppChatsManager.saveChannelAccess(peerParams[0], peerParams[1]) if (firstChar == 's') { AppChatsManager.saveIsMegagroup(peerParams[0]) } return { _: 'inputPeerChannel', channel_id: peerParams[0], access_hash: peerParams[1] || 0 } }else { return { _: 'inputPeerChat', chat_id: peerParams[0] } } } function getInputPeerByID (peerID) { if (!peerID) { return {_: 'inputPeerEmpty'} } if (peerID < 0) { var chatID = -peerID if (!AppChatsManager.isChannel(chatID)) { return { _: 'inputPeerChat', chat_id: chatID } } else { return { _: 'inputPeerChannel', channel_id: chatID, access_hash: AppChatsManager.getChat(chatID).access_hash || 0 } } } return { _: 'inputPeerUser', user_id: peerID, access_hash: AppUsersManager.getUser(peerID).access_hash || 0 } } function getPeerSearchText (peerID) { var text if (peerID > 0) { text = '%pu ' + AppUsersManager.getUserSearchText(peerID) } else if (peerID < 0) { var chat = AppChatsManager.getChat(-peerID) text = '%pg ' + (chat.title || '') } return text } function getPeerString (peerID) { if (peerID > 0) { return AppUsersManager.getUserString(peerID) } return AppChatsManager.getChatString(-peerID) } function getOutputPeer (peerID) { if (peerID > 0) { return {_: 'peerUser', user_id: peerID} } var chatID = -peerID if (AppChatsManager.isChannel(chatID)) { return {_: 'peerChannel', channel_id: chatID} } return {_: 'peerChat', chat_id: chatID} } function resolveUsername (username) { var searchUserName = SearchIndexManager.cleanUsername(username) if (searchUserName.match(/^\d+$/)) { return qSync.when(false) } var foundUserID var foundChatID, foundPeerID var foundUsername if (foundUserID = AppUsersManager.resolveUsername(searchUserName)) { foundUsername = AppUsersManager.getUser(foundUserID).username if (SearchIndexManager.cleanUsername(foundUsername) == searchUserName) { return qSync.when(foundUserID) } } if (foundChatID = AppChatsManager.resolveUsername(searchUserName)) { foundUsername = AppChatsManager.getChat(foundChatID).username if (SearchIndexManager.cleanUsername(foundUsername) == searchUserName) { return qSync.when(-foundChatID) } } return MtpApiManager.invokeApi('contacts.resolveUsername', {username: username}).then(function (resolveResult) { AppUsersManager.saveApiUsers(resolveResult.users) AppChatsManager.saveApiChats(resolveResult.chats) return getPeerID(resolveResult.peer) }) } function getPeerID (peerString) { if (angular.isObject(peerString)) { return peerString.user_id ? peerString.user_id : -(peerString.channel_id || peerString.chat_id) } var isUser = peerString.charAt(0) == 'u' var peerParams = peerString.substr(1).split('_') return isUser ? peerParams[0] : -peerParams[0] || 0 } function getPeer (peerID) { return peerID > 0 ? AppUsersManager.getUser(peerID) : AppChatsManager.getChat(-peerID) } function getPeerPhoto (peerID) { return peerID > 0 ? AppUsersManager.getUserPhoto(peerID) : AppChatsManager.getChatPhoto(-peerID) } function isChannel (peerID) { return (peerID < 0) && AppChatsManager.isChannel(-peerID) } function isMegagroup (peerID) { return (peerID < 0) && AppChatsManager.isMegagroup(-peerID) } function isAnyGroup (peerID) { return (peerID < 0) && !AppChatsManager.isBroadcast(-peerID) } function isBroadcast (id) { return isChannel(id) && !isMegagroup(id) } function isBot (peerID) { return (peerID > 0) && AppUsersManager.isBot(peerID) } return { getInputPeer: getInputPeer, getInputPeerByID: getInputPeerByID, getPeerSearchText: getPeerSearchText, getPeerString: getPeerString, getOutputPeer: getOutputPeer, getPeerID: getPeerID, getPeer: getPeer, getPeerPhoto: getPeerPhoto, resolveUsername: resolveUsername, isChannel: isChannel, isAnyGroup: isAnyGroup, isMegagroup: isMegagroup, isBroadcast: isBroadcast, isBot: isBot } }) .service('AppProfileManager', function ($q, $rootScope, AppUsersManager, AppChatsManager, AppMessagesIDsManager, AppPeersManager, AppPhotosManager, NotificationsManager, MtpApiManager, ApiUpdatesManager, RichTextProcessor, Storage) { var botInfos = {} var chatsFull = {} var chatFullPromises = {} var chatParticipantsPromises = {} function saveBotInfo (botInfo) { var botID = botInfo && botInfo.user_id if (!botID) { return false } var commands = {} angular.forEach(botInfo.commands, function (botCommand) { commands[botCommand.command] = botCommand.description }) return botInfos[botID] = { id: botID, version: botInfo.version, shareText: botInfo.share_text, description: botInfo.description, commands: commands } } function getProfile (id, override) { return MtpApiManager.invokeApi('users.getFullUser', { id: AppUsersManager.getUserInput(id) }).then(function (userFull) { if (override && override.phone_number) { userFull.user.phone = override.phone_number if (override.first_name || override.last_name) { userFull.user.first_name = override.first_name userFull.user.last_name = override.last_name } AppUsersManager.saveApiUser(userFull.user) } else { AppUsersManager.saveApiUser(userFull.user, true) } if (userFull.profile_photo) { AppPhotosManager.savePhoto(userFull.profile_photo, { user_id: id }) } if (userFull.about !== undefined) { userFull.rAbout = RichTextProcessor.wrapRichText(userFull.about, {noLinebreaks: true}) } NotificationsManager.savePeerSettings(id, userFull.notify_settings) if (userFull.bot_info) { userFull.bot_info = saveBotInfo(userFull.bot_info) } return userFull }) } function getPeerBots (peerID) { var peerBots = [] if (peerID >= 0 && !AppUsersManager.isBot(peerID) || (AppPeersManager.isChannel(peerID) && !AppPeersManager.isMegagroup(peerID))) { return $q.when(peerBots) } if (peerID >= 0) { return getProfile(peerID).then(function (userFull) { var botInfo = userFull.bot_info if (botInfo && botInfo._ != 'botInfoEmpty') { peerBots.push(botInfo) } return peerBots }) } return getChatFull(-peerID).then(function (chatFull) { angular.forEach(chatFull.bot_info, function (botInfo) { peerBots.push(saveBotInfo(botInfo)) }) return peerBots }) } function getChatFull (id) { if (AppChatsManager.isChannel(id)) { return getChannelFull(id) } if (chatsFull[id] !== undefined) { var chat = AppChatsManager.getChat(id) if (chat.version == chatsFull[id].participants.version || chat.pFlags.left) { return $q.when(chatsFull[id]) } } if (chatFullPromises[id] !== undefined) { return chatFullPromises[id] } // console.trace(dT(), 'Get chat full', id, AppChatsManager.getChat(id)) return chatFullPromises[id] = MtpApiManager.invokeApi('messages.getFullChat', { chat_id: AppChatsManager.getChatInput(id) }).then(function (result) { AppChatsManager.saveApiChats(result.chats) AppUsersManager.saveApiUsers(result.users) var fullChat = result.full_chat if (fullChat && fullChat.chat_photo.id) { AppPhotosManager.savePhoto(fullChat.chat_photo) } NotificationsManager.savePeerSettings(-id, fullChat.notify_settings) delete chatFullPromises[id] chatsFull[id] = fullChat $rootScope.$broadcast('chat_full_update', id) return fullChat }) } function getChatInviteLink (id, force) { return getChatFull(id).then(function (chatFull) { if (!force && chatFull.exported_invite && chatFull.exported_invite._ == 'chatInviteExported') { return chatFull.exported_invite.link } var promise if (AppChatsManager.isChannel(id)) { promise = MtpApiManager.invokeApi('channels.exportInvite', { channel: AppChatsManager.getChannelInput(id) }) } else { promise = MtpApiManager.invokeApi('messages.exportChatInvite', { chat_id: AppChatsManager.getChatInput(id) }) } return promise.then(function (exportedInvite) { if (chatsFull[id] !== undefined) { chatsFull[id].exported_invite = exportedInvite } return exportedInvite.link }) }) } function getChannelParticipants (id, filter, limit, offset) { filter = filter || {_: 'channelParticipantsRecent'} limit = limit || 200 offset = offset || 0 var promiseKey = [id, filter._, offset, limit].join('_') var promiseData = chatParticipantsPromises[promiseKey] if (filter._ == 'channelParticipantsRecent') { var chat = AppChatsManager.getChat(id) if (chat && chat.pFlags && ( chat.pFlags.kicked || chat.pFlags.broadcast && !chat.pFlags.creator && !chat.admin_rights )) { return $q.reject() } } var fetchParticipants = function (cachedParticipants) { var hash = 0 if (cachedParticipants) { var userIDs = [] angular.forEach(cachedParticipants, function (participant) { userIDs.push(participant.user_id) }) userIDs.sort() angular.forEach(userIDs, function (userID) { hash = ((hash * 20261) + 0x80000000 + userID) % 0x80000000 }) } return MtpApiManager.invokeApi('channels.getParticipants', { channel: AppChatsManager.getChannelInput(id), filter: filter, offset: offset, limit: limit, hash: hash }).then(function (result) { if (result._ == 'channels.channelParticipantsNotModified') { return cachedParticipants } AppUsersManager.saveApiUsers(result.users) return result.participants }) } var maybeAddSelf = function (participants) { var chat = AppChatsManager.getChat(id) var selfMustBeFirst = filter._ == 'channelParticipantsRecent' && !offset && !chat.pFlags.kicked && !chat.pFlags.left if (selfMustBeFirst) { participants = angular.copy(participants) var myID = AppUsersManager.getSelf().id var myIndex = false var myParticipant for (var i = 0, len = participants.length; i < len; i++) { if (participants[i].user_id == myID) { myIndex = i break } } if (myIndex !== false) { myParticipant = participants[i] participants.splice(i, 1) } else { myParticipant = {_: 'channelParticipantSelf', user_id: myID} } participants.unshift(myParticipant) } return participants } var timeNow = tsNow() if (promiseData !== undefined) { var promise = promiseData[1] if (promiseData[0] > timeNow - 60000) { return promise } var newPromise = promise.then(function (cachedParticipants) { return fetchParticipants(cachedParticipants).then(maybeAddSelf) }) chatParticipantsPromises[promiseKey] = [timeNow, newPromise] return newPromise } var newPromise = fetchParticipants().then(maybeAddSelf) chatParticipantsPromises[promiseKey] = [timeNow, newPromise] return newPromise } function getChannelFull (id, force) { if (chatsFull[id] !== undefined && !force) { return $q.when(chatsFull[id]) } if (chatFullPromises[id] !== undefined) { return chatFullPromises[id] } return chatFullPromises[id] = MtpApiManager.invokeApi('channels.getFullChannel', { channel: AppChatsManager.getChannelInput(id) }).then(function (result) { AppChatsManager.saveApiChats(result.chats) AppUsersManager.saveApiUsers(result.users) var fullChannel = result.full_chat if (fullChannel && fullChannel.chat_photo.id) { AppPhotosManager.savePhoto(fullChannel.chat_photo) } NotificationsManager.savePeerSettings(-id, fullChannel.notify_settings) if (fullChannel.pinned_msg_id) { fullChannel.pinned_msg_id = AppMessagesIDsManager.getFullMessageID(fullChannel.pinned_msg_id, id) } delete chatFullPromises[id] chatsFull[id] = fullChannel $rootScope.$broadcast('chat_full_update', id) return fullChannel }, function (error) { switch (error.type) { case 'CHANNEL_PRIVATE': var channel = AppChatsManager.getChat(id) channel = {_: 'channelForbidden', access_hash: channel.access_hash, title: channel.title} ApiUpdatesManager.processUpdateMessage({ _: 'updates', updates: [{ _: 'updateChannel', channel_id: id }], chats: [channel], users: [] }) break } return $q.reject(error) }) } function invalidateChannelParticipants(id) { delete chatsFull[id] delete chatFullPromises[id] angular.forEach(chatParticipantsPromises, function (val, key) { if (key.split('_')[0] == id) { delete chatParticipantsPromises[key] } }) $rootScope.$broadcast('chat_full_update', id) } function getChannelPinnedMessage(id) { return getChannelFull(id).then(function (fullChannel) { var pinnedMessageID = fullChannel && fullChannel.pinned_msg_id if (!pinnedMessageID) { return false } return Storage.get('pinned_hidden' + id).then(function (hiddenMessageID) { if (AppMessagesIDsManager.getMessageLocalID(pinnedMessageID) == hiddenMessageID) { return false } return pinnedMessageID }) }) } function hideChannelPinnedMessage(id, pinnedMessageID) { var setKeys = {} setKeys['pinned_hidden' + id] = AppMessagesIDsManager.getMessageLocalID(pinnedMessageID) Storage.set(setKeys) $rootScope.$broadcast('peer_pinned_message', -id) } $rootScope.$on('apiUpdate', function (e, update) { // console.log('on apiUpdate', update) switch (update._) { case 'updateChatParticipants': var participants = update.participants var chatFull = chatsFull[participants.id] if (chatFull !== undefined) { chatFull.participants = update.participants $rootScope.$broadcast('chat_full_update', chatID) } break case 'updateChatParticipantAdd': var chatFull = chatsFull[update.chat_id] if (chatFull !== undefined) { var participants = chatFull.participants.participants || [] for (var i = 0, length = participants.length; i < length; i++) { if (participants[i].user_id == update.user_id) { return } } participants.push({ _: 'chatParticipant', user_id: update.user_id, inviter_id: update.inviter_id, date: tsNow(true) }) chatFull.participants.version = update.version $rootScope.$broadcast('chat_full_update', update.chat_id) } break case 'updateChatParticipantDelete': var chatFull = chatsFull[update.chat_id] if (chatFull !== undefined) { var participants = chatFull.participants.participants || [] for (var i = 0, length = participants.length; i < length; i++) { if (participants[i].user_id == update.user_id) { participants.splice(i, 1) chatFull.participants.version = update.version $rootScope.$broadcast('chat_full_update', update.chat_id) return } } } break case 'updateChannelPinnedMessage': var channelID = update.channel_id var fullChannel = chatsFull[channelID] if (fullChannel !== undefined) { fullChannel.pinned_msg_id = AppMessagesIDsManager.getFullMessageID(update.id, channelID) $rootScope.$broadcast('peer_pinned_message', -channelID) } break } }) $rootScope.$on('chat_update', function (e, chatID) { var fullChat = chatsFull[chatID] var chat = AppChatsManager.getChat(chatID) if (!chat.photo || !fullChat) { return } var emptyPhoto = chat.photo._ == 'chatPhotoEmpty' if (emptyPhoto != (fullChat.chat_photo._ == 'photoEmpty')) { delete chatsFull[chatID] $rootScope.$broadcast('chat_full_update', chatID) return } if (emptyPhoto) { return } var smallUserpic = chat.photo.photo_small var smallPhotoSize = AppPhotosManager.choosePhotoSize(fullChat.chat_photo, 0, 0) if (!angular.equals(smallUserpic, smallPhotoSize.location)) { delete chatsFull[chatID] $rootScope.$broadcast('chat_full_update', chatID) } }) return { getPeerBots: getPeerBots, getProfile: getProfile, getChatInviteLink: getChatInviteLink, getChatFull: getChatFull, getChannelFull: getChannelFull, getChannelParticipants: getChannelParticipants, invalidateChannelParticipants: invalidateChannelParticipants, getChannelPinnedMessage: getChannelPinnedMessage, hideChannelPinnedMessage: hideChannelPinnedMessage } }) .service('AppPhotosManager', function ($modal, $window, $rootScope, MtpApiManager, MtpApiFileManager, AppUsersManager, FileManager) { var photos = {} var windowW = $(window).width() var windowH = $(window).height() function savePhoto (apiPhoto, context) { if (context) { angular.extend(apiPhoto, context) } 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) { if (Config.Navigator.retina) { width *= 2 height *= 2 } var bestPhotoSize = {_: 'photoSizeEmpty'} var 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 } }) // console.log('choosing', photo, width, height, bestPhotoSize) return bestPhotoSize } function getUserPhotos (userID, maxID, limit) { var inputUser = AppUsersManager.getUserInput(userID) return MtpApiManager.invokeApi('photos.getUserPhotos', { user_id: inputUser, offset: 0, limit: limit || 20, max_id: maxID || 0 }).then(function (photosResult) { AppUsersManager.saveApiUsers(photosResult.users) var photoIDs = [] var context = {user_id: userID} for (var i = 0; i < photosResult.photos.length; i++) { savePhoto(photosResult.photos[i], context) photoIDs.push(photosResult.photos[i].id) } return { count: photosResult.count || photosResult.photos.length, photos: photoIDs } }) } function preloadPhoto (photoID) { if (!photos[photoID]) { return } var photo = photos[photoID] var fullWidth = $(window).width() - (Config.Mobile ? 20 : 32) var fullHeight = $($window).height() - (Config.Mobile ? 150 : 116) if (fullWidth > 800) { fullWidth -= 208 } var fullPhotoSize = choosePhotoSize(photo, fullWidth, fullHeight) if (fullPhotoSize && !fullPhotoSize.preloaded) { fullPhotoSize.preloaded = true if (fullPhotoSize.size) { MtpApiFileManager.downloadFile(fullPhotoSize.location.dc_id, { _: 'inputFileLocation', volume_id: fullPhotoSize.location.volume_id, local_id: fullPhotoSize.location.local_id, secret: fullPhotoSize.location.secret }, fullPhotoSize.size) } else { MtpApiFileManager.downloadSmallFile(fullPhotoSize.location) } } } $rootScope.preloadPhoto = preloadPhoto function getPhoto (photoID) { return photos[photoID] || {_: 'photoEmpty'} } function wrapForHistory (photoID, options) { options = options || {} var photo = angular.copy(photos[photoID]) || {_: 'photoEmpty'} var width = options.website ? 64 : Math.min(windowW - 80, Config.Mobile ? 210 : 260) var height = options.website ? 64 : Math.min(windowH - 100, Config.Mobile ? 210 : 260) var thumbPhotoSize = choosePhotoSize(photo, width, height) var thumb = { placeholder: 'img/placeholders/PhotoThumbConversation.gif', width: width, height: height } if (options.website && Config.Mobile) { width = 50 height = 50 } // console.log('chosen photo size', photoID, thumbPhotoSize) if (thumbPhotoSize && thumbPhotoSize._ != 'photoSizeEmpty') { var dim = calcImageInBox(thumbPhotoSize.w, thumbPhotoSize.h, width, height) thumb.width = dim.w thumb.height = dim.h thumb.location = thumbPhotoSize.location thumb.size = thumbPhotoSize.size } else { thumb.width = 100 thumb.height = 100 } photo.thumb = thumb return photo } function wrapForFull (photoID) { var photo = wrapForHistory(photoID) var fullWidth = $(window).width() - (Config.Mobile ? 0 : 32) var fullHeight = $($window).height() - (Config.Mobile ? 0 : 116) if (!Config.Mobile && fullWidth > 800) { fullWidth -= 208 } var fullPhotoSize = choosePhotoSize(photo, fullWidth, fullHeight) var full = { placeholder: 'img/placeholders/PhotoThumbModal.gif' } full.width = fullWidth full.height = fullHeight if (fullPhotoSize && fullPhotoSize._ != 'photoSizeEmpty') { var wh = calcImageInBox(fullPhotoSize.w, fullPhotoSize.h, fullWidth, fullHeight, true) full.width = wh.w full.height = wh.h full.modalWidth = Math.max(full.width, Math.min(400, fullWidth)) full.location = fullPhotoSize.location full.size = fullPhotoSize.size } photo.full = full return photo } function openPhoto (photoID, list) { if (!photoID || photoID === '0') { return false } var scope = $rootScope.$new(true) scope.photoID = photoID var controller = 'PhotoModalController' if (list && list.p > 0) { controller = 'UserpicModalController' scope.userID = list.p } else if (list && list.p < 0) { controller = 'ChatpicModalController' scope.chatID = -list.p } else if (list && list.m > 0) { scope.messageID = list.m if (list.w) { scope.webpageID = list.w } } var modalInstance = $modal.open({ templateUrl: templateUrl('photo_modal'), windowTemplateUrl: templateUrl('media_modal_layout'), controller: controller, scope: scope, windowClass: 'photo_modal_window' }) } function downloadPhoto (photoID) { var photo = photos[photoID] var ext = 'jpg' var mimeType = 'image/jpeg' var fileName = 'photo' + photoID + '.' + ext var fullWidth = Math.max(screen.width || 0, $(window).width() - 36, 800) var fullHeight = Math.max(screen.height || 0, $($window).height() - 150, 800) var fullPhotoSize = choosePhotoSize(photo, fullWidth, fullHeight) var inputFileLocation = { _: 'inputFileLocation', volume_id: fullPhotoSize.location.volume_id, local_id: fullPhotoSize.location.local_id, secret: fullPhotoSize.location.secret } FileManager.chooseSave(fileName, ext, mimeType).then(function (writableFileEntry) { if (writableFileEntry) { MtpApiFileManager.downloadFile( fullPhotoSize.location.dc_id, inputFileLocation, fullPhotoSize.size, { mime: mimeType, toFileEntry: writableFileEntry }).then(function () { // console.log('file save done') }, function (e) { console.log('photo download failed', e) }) } }, function () { var cachedBlob = MtpApiFileManager.getCachedFile(inputFileLocation) if (cachedBlob) { return FileManager.download(cachedBlob, mimeType, fileName) } MtpApiFileManager.downloadFile( fullPhotoSize.location.dc_id, inputFileLocation, fullPhotoSize.size, {mime: mimeType} ).then(function (blob) { FileManager.download(blob, mimeType, fileName) }, function (e) { console.log('photo download failed', e) }) }) } $rootScope.openPhoto = openPhoto return { savePhoto: savePhoto, preloadPhoto: preloadPhoto, getUserPhotos: getUserPhotos, getPhoto: getPhoto, choosePhotoSize: choosePhotoSize, wrapForHistory: wrapForHistory, wrapForFull: wrapForFull, openPhoto: openPhoto, downloadPhoto: downloadPhoto } }) .service('AppWebPagesManager', function ($modal, $sce, $window, $rootScope, MtpApiManager, AppPhotosManager, AppDocsManager, RichTextProcessor) { var webpages = {} var pendingWebPages = {} function saveWebPage (apiWebPage, messageID, mediaContext) { if (apiWebPage.photo && apiWebPage.photo._ === 'photo') { AppPhotosManager.savePhoto(apiWebPage.photo, mediaContext) } else { delete apiWebPage.photo } if (apiWebPage.document && apiWebPage.document._ === 'document') { AppDocsManager.saveDoc(apiWebPage.document, mediaContext) } else { if (apiWebPage.type == 'document') { delete apiWebPage.type } delete apiWebPage.document } var siteName = apiWebPage.site_name var shortTitle = apiWebPage.title || apiWebPage.author || siteName || '' if (siteName && shortTitle == siteName) { delete apiWebPage.site_name } if (shortTitle.length > 100) { shortTitle = shortTitle.substr(0, 80) + '...' } apiWebPage.rTitle = RichTextProcessor.wrapRichText(shortTitle, {noLinks: true, noLinebreaks: true}) var contextHashtag = '' if (siteName == 'GitHub') { var matches = apiWebPage.url.match(/(https?:\/\/github\.com\/[^\/]+\/[^\/]+)/) if (matches) { contextHashtag = matches[0] + '/issues/{1}' } } // delete apiWebPage.description var shortDescriptionText = (apiWebPage.description || '') if (shortDescriptionText.length > 180) { shortDescriptionText = shortDescriptionText.substr(0, 150).replace(/(\n|\s)+$/, '') + '...' } apiWebPage.rDescription = RichTextProcessor.wrapRichText( shortDescriptionText, { contextSite: siteName || 'external', contextHashtag: contextHashtag } ) if (apiWebPage.type != 'photo' && apiWebPage.type != 'video' && apiWebPage.type != 'gif' && apiWebPage.type != 'document' && !apiWebPage.description && apiWebPage.photo) { apiWebPage.type = 'photo' } if (messageID) { if (pendingWebPages[apiWebPage.id] === undefined) { pendingWebPages[apiWebPage.id] = {} } pendingWebPages[apiWebPage.id][messageID] = true webpages[apiWebPage.id] = apiWebPage } if (webpages[apiWebPage.id] === undefined) { webpages[apiWebPage.id] = apiWebPage } else { safeReplaceObject(webpages[apiWebPage.id], apiWebPage) } if (!messageID && pendingWebPages[apiWebPage.id] !== undefined) { var msgs = [] angular.forEach(pendingWebPages[apiWebPage.id], function (t, msgID) { msgs.push(msgID) }) $rootScope.$broadcast('webpage_updated', { id: apiWebPage.id, msgs: msgs }) } } function openEmbed (webpageID, messageID) { var scope = $rootScope.$new(true) scope.webpageID = webpageID scope.messageID = messageID $modal.open({ templateUrl: templateUrl('embed_modal'), windowTemplateUrl: templateUrl('media_modal_layout'), controller: 'EmbedModalController', scope: scope, windowClass: 'photo_modal_window' }) } function wrapForHistory (webPageID) { var webPage = angular.copy(webpages[webPageID]) || {_: 'webPageEmpty'} if (webPage.photo && webPage.photo.id) { webPage.photo = AppPhotosManager.wrapForHistory(webPage.photo.id, {website: webPage.type != 'photo' && webPage.type != 'video'}) } if (webPage.document && webPage.document.id) { webPage.document = AppDocsManager.wrapForHistory(webPage.document.id) } return webPage } function wrapForFull (webPageID) { var webPage = wrapForHistory(webPageID) if (!webPage.embed_url) { return webPage } var fullWidth = $(window).width() - (Config.Mobile ? 0 : 10) var fullHeight = $($window).height() - (Config.Mobile ? 92 : 150) if (!Config.Mobile && fullWidth > 800) { fullWidth -= 208 } var full = { width: fullWidth, height: fullHeight } if (!webPage.embed_width || !webPage.embed_height) { full.height = full.width = Math.min(fullWidth, fullHeight) } else { var wh = calcImageInBox(webPage.embed_width, webPage.embed_height, fullWidth, fullHeight) full.width = wh.w full.height = wh.h } var embedTag = Config.Modes.chrome_packed ? 'webview' : 'iframe' var embedType = webPage.embed_type != 'iframe' ? webPage.embed_type || 'text/html' : 'text/html' var embedHtml = '<' + embedTag + ' src="' + encodeEntities(webPage.embed_url) + '" type="' + encodeEntities(embedType) + '" frameborder="0" border="0" webkitallowfullscreen mozallowfullscreen allowfullscreen width="' + full.width + '" height="' + full.height + '" style="width: ' + full.width + 'px; height: ' + full.height + 'px;">' full.html = $sce.trustAs('html', embedHtml) webPage.full = full return webPage } $rootScope.$on('apiUpdate', function (e, update) { switch (update._) { case 'updateWebPage': saveWebPage(update.webpage) break } }) return { saveWebPage: saveWebPage, openEmbed: openEmbed, wrapForFull: wrapForFull, wrapForHistory: wrapForHistory } }) .service('AppGamesManager', function ($modal, $sce, $window, $rootScope, MtpApiManager, AppPhotosManager, AppDocsManager, RichTextProcessor) { var games = {} function saveGame (apiGame, messageID, mediaContext) { if (apiGame.photo && apiGame.photo._ === 'photo') { AppPhotosManager.savePhoto(apiGame.photo, mediaContext) } else { delete apiGame.photo } if (apiGame.document && apiGame.document._ === 'document') { AppDocsManager.saveDoc(apiGame.document, mediaContext) } else { delete apiGame.document } apiGame.rTitle = RichTextProcessor.wrapRichText(apiGame.title, {noLinks: true, noLinebreaks: true}) apiGame.rDescription = RichTextProcessor.wrapRichText( apiGame.description || '', {} ) if (games[apiGame.id] === undefined) { games[apiGame.id] = apiGame } else { safeReplaceObject(games[apiGame.id], apiGame) } } function openGame (gameID, messageID, embedUrl) { var scope = $rootScope.$new(true) scope.gameID = gameID scope.messageID = messageID scope.embedUrl = embedUrl $modal.open({ templateUrl: templateUrl('game_modal'), windowTemplateUrl: templateUrl('media_modal_layout'), controller: 'GameModalController', scope: scope, windowClass: 'photo_modal_window mobile_modal' }) } function wrapForHistory (gameID) { var game = angular.copy(games[gameID]) || {_: 'gameEmpty'} if (game.photo && game.photo.id) { game.photo = AppPhotosManager.wrapForHistory(game.photo.id) } if (game.document && game.document.id) { game.document = AppDocsManager.wrapForHistory(game.document.id) } return game } function wrapForFull (gameID, msgID, embedUrl) { var game = wrapForHistory(gameID) var fullWidth = $(window).width() - (Config.Mobile ? 0 : 10) var fullHeight = $($window).height() - (Config.Mobile ? 51 : 150) if (!Config.Mobile && fullWidth > 800) { fullWidth -= 208 } var full = { width: fullWidth, height: fullHeight } var embedTag = Config.Modes.chrome_packed ? 'webview' : 'iframe' var embedType = 'text/html' var embedHtml = '<' + embedTag + ' src="' + encodeEntities(embedUrl) + '" type="' + encodeEntities(embedType) + '" frameborder="0" border="0" webkitallowfullscreen mozallowfullscreen allowfullscreen width="' + full.width + '" height="' + full.height + '" style="width: ' + full.width + 'px; height: ' + full.height + 'px;" sandbox="allow-scripts allow-same-origin">' full.html = $sce.trustAs('html', embedHtml) game.full = full return game } return { saveGame: saveGame, openGame: openGame, wrapForFull: wrapForFull, wrapForHistory: wrapForHistory } }) .service('AppDocsManager', function ($sce, $rootScope, $modal, $window, $q, $timeout, RichTextProcessor, MtpApiFileManager, FileManager, qSync) { var docs = {} var docsForHistory = {} var windowW = $(window).width() var windowH = $(window).height() function saveDoc (apiDoc, context) { docs[apiDoc.id] = apiDoc if (context) { angular.extend(apiDoc, context) } 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' } if (apiDoc.thumb && apiDoc.thumb._ == 'photoSizeEmpty') { delete apiDoc.thumb } angular.forEach(apiDoc.attributes, function (attribute) { switch (attribute._) { case 'documentAttributeFilename': apiDoc.file_name = attribute.file_name break case 'documentAttributeAudio': apiDoc.duration = attribute.duration apiDoc.audioTitle = attribute.title apiDoc.audioPerformer = attribute.performer apiDoc.type = attribute.pFlags.voice ? 'voice' : 'audio' break case 'documentAttributeVideo': apiDoc.duration = attribute.duration apiDoc.w = attribute.w apiDoc.h = attribute.h if (apiDoc.thumb && attribute.pFlags.round_message) { apiDoc.type = 'round' } else if (apiDoc.thumb) { apiDoc.type = 'video' } break case 'documentAttributeSticker': apiDoc.sticker = true if (attribute.alt !== undefined) { apiDoc.stickerEmojiRaw = attribute.alt apiDoc.stickerEmoji = RichTextProcessor.wrapRichText(apiDoc.stickerEmojiRaw, {noLinks: true, noLinebreaks: true}) } if (attribute.stickerset) { if (attribute.stickerset._ == 'inputStickerSetEmpty') { delete attribute.stickerset } else if (attribute.stickerset._ == 'inputStickerSetID') { apiDoc.stickerSetInput = attribute.stickerset } } if (apiDoc.thumb && apiDoc.mime_type == 'image/webp') { apiDoc.type = 'sticker' } break case 'documentAttributeImageSize': apiDoc.w = attribute.w apiDoc.h = attribute.h break case 'documentAttributeAnimated': if ((apiDoc.mime_type == 'image/gif' || apiDoc.mime_type == 'video/mp4') && apiDoc.thumb) { apiDoc.type = 'gif' } apiDoc.animated = true break } }) if (!apiDoc.mime_type) { switch (apiDoc.type) { case 'gif': apiDoc.mime_type = 'video/mp4' break case 'video': case 'round': apiDoc.mime_type = 'video/mp4' break case 'sticker': apiDoc.mime_type = 'image/webp' break case 'audio': apiDoc.mime_type = 'audio/mpeg' break case 'voice': apiDoc.mime_type = 'audio/ogg' break default: apiDoc.mime_type = 'application/octet-stream' break } } if (!apiDoc.file_name) { apiDoc.file_name = '' } if (apiDoc._ == 'documentEmpty') { apiDoc.size = 0 } } function getDoc (docID) { return docs[docID] || {_: 'documentEmpty'} } function hasDoc (docID) { return docs[docID] !== undefined } function getFileName (doc) { if (doc.file_name) { return doc.file_name } var fileExt = '.' + doc.mime_type.split('/')[1] if (fileExt == '.octet-stream') { fileExt = '' } return 't_' + (doc.type || 'file') + doc.id + fileExt } function wrapForHistory (docID) { if (docsForHistory[docID] !== undefined) { return docsForHistory[docID] } var doc = angular.copy(docs[docID]) var thumbPhotoSize = doc.thumb var inlineImage = false, boxWidth var boxHeight, thumb var dim switch (doc.type) { case 'video': boxWidth = Math.min(windowW - 80, Config.Mobile ? 210 : 150), boxHeight = Math.min(windowH - 100, Config.Mobile ? 210 : 150) break case 'sticker': inlineImage = true boxWidth = Math.min(windowW - 80, Config.Mobile ? 128 : 192) boxHeight = Math.min(windowH - 100, Config.Mobile ? 128 : 192) break case 'gif': inlineImage = true boxWidth = Math.min(windowW - 80, Config.Mobile ? 210 : 260) boxHeight = Math.min(windowH - 100, Config.Mobile ? 210 : 260) break case 'round': inlineImage = true boxWidth = Math.min(windowW - 80, 200) boxHeight = Math.min(windowH - 100, 200) break default: boxWidth = boxHeight = 100 } if (inlineImage && doc.w && doc.h) { dim = calcImageInBox(doc.w, doc.h, boxWidth, boxHeight) } else if (thumbPhotoSize) { dim = calcImageInBox(thumbPhotoSize.w, thumbPhotoSize.h, boxWidth, boxHeight) } if (dim) { thumb = { width: dim.w, height: dim.h } if (thumbPhotoSize) { thumb.location = thumbPhotoSize.location thumb.size = thumbPhotoSize.size } } else { thumb = false } doc.thumb = thumb doc.withPreview = !Config.Mobile && doc.mime_type.match(/^image\/(gif|png|jpeg|jpg|bmp|tiff)/) ? 1 : 0 return docsForHistory[docID] = doc } function updateDocDownloaded (docID) { var doc = docs[docID] var historyDoc = docsForHistory[docID] || doc || {} var inputFileLocation = { _: 'inputDocumentFileLocation', id: docID, access_hash: doc.access_hash, version: doc.version, file_name: getFileName(doc) } if (historyDoc.downloaded === undefined) { MtpApiFileManager.getDownloadedFile(inputFileLocation, doc.size).then(function () { historyDoc.downloaded = true }, function () { historyDoc.downloaded = false }) } } function downloadDoc (docID, toFileEntry) { var doc = docs[docID] var historyDoc = docsForHistory[docID] || doc || {} var inputFileLocation = { _: 'inputDocumentFileLocation', id: docID, access_hash: doc.access_hash, version: doc.version, file_name: getFileName(doc) } if (doc._ == 'documentEmpty') { return $q.reject() } if (historyDoc.downloaded && !toFileEntry) { var cachedBlob = MtpApiFileManager.getCachedFile(inputFileLocation) if (cachedBlob) { return qSync.when(cachedBlob) } } historyDoc.progress = {enabled: !historyDoc.downloaded, percent: 1, total: doc.size} var downloadPromise = MtpApiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, { mime: doc.mime_type || 'application/octet-stream', toFileEntry: toFileEntry }) downloadPromise.then(function (blob) { if (blob) { FileManager.getFileCorrectUrl(blob, doc.mime_type).then(function (url) { var trustedUrl = $sce.trustAsResourceUrl(url) historyDoc.url = trustedUrl doc.url = trustedUrl }) historyDoc.downloaded = true } historyDoc.progress.percent = 100 $timeout(function () { delete historyDoc.progress }) // console.log('file save done') }, function (e) { console.log('document download failed', e) historyDoc.progress.enabled = false }, function (progress) { console.log('dl progress', progress) historyDoc.progress.enabled = true historyDoc.progress.done = progress.done historyDoc.progress.percent = Math.max(1, Math.floor(100 * progress.done / progress.total)) $rootScope.$broadcast('history_update') }) historyDoc.progress.cancel = downloadPromise.cancel return downloadPromise } function openDoc (docID, messageID) { var scope = $rootScope.$new(true) scope.docID = docID scope.messageID = messageID var modalInstance = $modal.open({ templateUrl: templateUrl('document_modal'), windowTemplateUrl: templateUrl('media_modal_layout'), controller: 'DocumentModalController', scope: scope, windowClass: 'document_modal_window' }) } function saveDocFile (docID) { var doc = docs[docID] var historyDoc = docsForHistory[docID] || doc || {} var mimeType = doc.mime_type var fileName = getFileName(doc) var ext = (fileName.split('.', 2) || [])[1] || '' FileManager.chooseSave(getFileName(doc), ext, doc.mime_type).then(function (writableFileEntry) { if (writableFileEntry) { downloadDoc(docID, writableFileEntry) } }, function () { downloadDoc(docID).then(function (blob) { FileManager.download(blob, doc.mime_type, fileName) }) }) } function wrapVideoForFull (docID) { var doc = wrapForHistory(docID) var fullWidth = Math.min($(window).width() - (Config.Mobile ? 0 : 60), 542) var fullHeight = $(window).height() - (Config.Mobile ? 92 : 150) var full = { placeholder: 'img/placeholders/docThumbModal.gif', width: fullWidth, height: fullHeight } if (!doc.w || !doc.h) { full.height = full.width = Math.min(fullWidth, fullHeight) } else { var dim = calcImageInBox(doc.w, doc.h, fullWidth, fullHeight) full.width = dim.w full.height = dim.h } doc.full = full doc.fullThumb = angular.copy(doc.thumb) doc.fullThumb.width = full.width doc.fullThumb.height = full.height return doc } function openVideo (docID, messageID) { var scope = $rootScope.$new(true) scope.docID = docID scope.messageID = messageID return $modal.open({ templateUrl: templateUrl('video_modal'), windowTemplateUrl: templateUrl('media_modal_layout'), controller: 'VideoModalController', scope: scope, windowClass: 'video_modal_window' }) } return { saveDoc: saveDoc, getDoc: getDoc, hasDoc: hasDoc, wrapForHistory: wrapForHistory, wrapVideoForFull: wrapVideoForFull, updateDocDownloaded: updateDocDownloaded, downloadDoc: downloadDoc, openDoc: openDoc, openVideo: openVideo, saveDocFile: saveDocFile } }) .service('AppStickersManager', function ($q, $rootScope, $modal, _, FileManager, MtpApiManager, AppDocsManager, Storage, ApiUpdatesManager) { var started = false var applied = false var currentStickerSets = [] var emojiIndex = {} $rootScope.$on('apiUpdate', function (e, update) { if (update._ != 'updateStickerSets' && update._ != 'updateNewStickerSet' && update._ != 'updateDelStickerSet' && update._ != 'updateStickerSetsOrder') { return false } return Storage.get('all_stickers').then(function (stickers) { if (!stickers || stickers.layer != Config.Schema.API.layer) { $rootScope.$broadcast('stickers_changed') } switch (update._) { case 'updateNewStickerSet': var fullSet = update.stickerset var set = fullSet.set if (set.pFlags.masks) { return false } var pos = false for (var i = 0, len = stickers.sets.length; i < len; i++) { if (stickers.sets[i].id == set.id) { pos = i break } } if (pos !== false) { stickers.sets.splice(pos, 1) } set.pFlags.installed = true stickers.sets.unshift(set) stickers.fullSets[set.id] = fullSet indexStickerSetEmoticons(fullSet) break case 'updateDelStickerSet': var set for (var i = 0, len = stickers.sets.length; i < len; i++) { set = stickers.sets[i] if (set.id == update.id) { set.pFlags.installed = false stickers.sets.splice(i, 1) break } } delete stickers.fullSets[update.id] break case 'updateStickerSetsOrder': if (update.pFlags.masks) { return } var order = update.order stickers.sets.sort(function (a, b) { return order.indexOf(a.id) - order.indexOf(b.id) }) break } stickers.hash = getStickerSetsHash(stickers.sets) stickers.date = 0 Storage.set({all_stickers: stickers}).then(function () { $rootScope.$broadcast('stickers_changed') }) }) }) return { start: start, getStickers: getStickers, openStickersetLink: openStickersetLink, openStickerset: openStickerset, installStickerset: installStickerset, pushPopularSticker: pushPopularSticker, searchStickers: searchStickers, getStickerset: getStickerset } function start () { if (!started) { started = true setTimeout(getStickers, 1000) } } function getStickers (force) { return Storage.get('all_stickers').then(function (stickers) { var layer = Config.Schema.API.layer if (stickers.layer != layer || stickers.emojiIndex === undefined) { stickers = false } if (stickers && stickers.date > tsNow(true) && !force) { emojiIndex = stickers.emojiIndex return processRawStickers(stickers) } return MtpApiManager.invokeApi('messages.getAllStickers', { hash: stickers && stickers.hash || '' }).then(function (newStickers) { var notModified = newStickers._ == 'messages.allStickersNotModified' if (notModified) { newStickers = stickers } newStickers.date = tsNow(true) + 3600 newStickers.layer = layer delete newStickers._ if (notModified) { Storage.set({all_stickers: newStickers}) emojiIndex = newStickers.emojiIndex return processRawStickers(newStickers) } return getStickerSets(newStickers, stickers && stickers.fullSets).then(function () { Storage.set({all_stickers: newStickers}) return processRawStickers(newStickers) }) }) }) } function processRawStickers (stickers) { if (applied !== stickers.hash) { applied = stickers.hash var i var j, len1 var len2, doc var set, docIDs var documents currentStickerSets = [] len1 = stickers.sets.length for (i = 0; i < len1; i++) { set = stickers.sets[i] if (set.pFlags.disabled) { continue } documents = stickers.fullSets[set.id].documents len2 = documents.length docIDs = [] for (j = 0; j < len2; j++) { doc = documents[j] AppDocsManager.saveDoc(doc) docIDs.push(doc.id) } set.docIDs = docIDs currentStickerSets.push(set) } } return getPopularStickers().then(function (popularStickers) { var resultStickersets = currentStickerSets if (popularStickers.length) { resultStickersets = currentStickerSets.slice() var docIDs = [] var i var len for (i = 0, len = popularStickers.length; i < len; i++) { docIDs.push(popularStickers[i].id) } resultStickersets.unshift({ id: 0, title: _('im_stickers_tab_recent_raw'), short_name: '', docIDs: docIDs }) } return resultStickersets }) } function indexStickerSetEmoticons(fullSet) { angular.forEach(fullSet.packs, function (pack) { var emoji = pack.emoticon var emojiCode = false while (emoji.length) { emojiCode = EmojiHelper.emojiMap[emoji] if (emojiCode !== undefined) { break } emoji = emoji.substr(0, -1) } // console.warn('index', fullSet, pack, emojiCode) if (emojiCode === undefined) { return } var stickersList = emojiIndex[emojiCode] if (stickersList === undefined) { emojiIndex[emojiCode] = stickersList = [] } angular.forEach(pack.documents, function (docID) { if (stickersList.indexOf(docID) === -1) { stickersList.push(docID) } }) }) } function searchStickers(emojiCode) { return getPopularStickers().then(function () { // console.warn('search', emojiCode, emojiIndex, emojiIndex[emojiCode]) var stickersList = emojiIndex[emojiCode] var result = [] if (stickersList === undefined) { return result } var setIDs = [] angular.forEach(currentStickerSets, function (set) { setIDs.push(set.id) }) angular.forEach(stickersList, function (docID) { var doc = AppDocsManager.getDoc(docID) if (!doc || !doc.stickerSetInput) { return } var setID = doc.stickerSetInput.id if (setIDs.indexOf(setID) == -1) { return } result.push(doc) }) result.sort(function (doc1, doc2) { return setIDs.indexOf(doc1.stickerSetInput.id) - setIDs.indexOf(doc2.stickerSetInput.id) }) return result }) } function getStickerSets (allStickers, prevCachedSets) { var promises = [] var cachedSets = prevCachedSets || allStickers.fullSets || {} allStickers.fullSets = {} emojiIndex = allStickers.emojiIndex = {} angular.forEach(allStickers.sets, function (shortSet) { var fullSet = cachedSets[shortSet.id] if (fullSet && fullSet.set.hash == shortSet.hash) { allStickers.fullSets[shortSet.id] = fullSet indexStickerSetEmoticons(fullSet) } else { var promise = MtpApiManager.invokeApi('messages.getStickerSet', { stickerset: { _: 'inputStickerSetID', id: shortSet.id, access_hash: shortSet.access_hash } }).then(function (fullSet) { allStickers.fullSets[shortSet.id] = fullSet indexStickerSetEmoticons(fullSet) }) promises.push(promise) } }) return $q.all(promises) } function getPopularStickers () { return Storage.get('stickers_popular').then(function (popStickers) { var result = [] var i, len var docID if (popStickers && popStickers.length) { for (i = 0, len = popStickers.length; i < len; i++) { docID = popStickers[i][0] if (AppDocsManager.hasDoc(docID)) { result.push({id: docID, rate: popStickers[i][1]}) } } } return result }) } function pushPopularSticker (id) { getPopularStickers().then(function (popularStickers) { var exists = false var count = popularStickers.length var result = [] for (var i = 0; i < count; i++) { if (popularStickers[i].id == id) { exists = true popularStickers[i].rate++ } result.push([popularStickers[i].id, popularStickers[i].rate]) } if (exists) { result.sort(function (a, b) { return b[1] - a[1] }) } else { if (result.length > 15) { result = result.slice(0, 15) } result.push([id, 1]) } ConfigStorage.set({stickers_popular: result}) }) } function getStickerset (inputStickerset) { return MtpApiManager.invokeApi('messages.getStickerSet', { stickerset: inputStickerset }).then(function (result) { for (var i = 0; i < result.documents.length; i++) { AppDocsManager.saveDoc(result.documents[i]) } return result }) } function installStickerset (fullSet, uninstall) { var method = uninstall ? 'messages.uninstallStickerSet' : 'messages.installStickerSet' var inputStickerset = { _: 'inputStickerSetID', id: fullSet.set.id, access_hash: fullSet.set.access_hash } return MtpApiManager.invokeApi(method, { stickerset: inputStickerset, disabled: false }).then(function (result) { var update if (uninstall) { update = {_: 'updateDelStickerSet', id: fullSet.set.id} } else { update = {_: 'updateNewStickerSet', stickerset: fullSet} } ApiUpdatesManager.processUpdateMessage({ _: 'updateShort', update: update }) }) } function openStickersetLink (shortName) { return openStickerset({ _: 'inputStickerSetShortName', short_name: shortName }) } function openStickerset (inputStickerset) { var scope = $rootScope.$new(true) scope.inputStickerset = inputStickerset var modal = $modal.open({ templateUrl: templateUrl('stickerset_modal'), controller: 'StickersetModalController', scope: scope, windowClass: 'stickerset_modal_window mobile_modal' }) } function getStickerSetsHash (stickerSets) { var acc = 0 var set for (var i = 0; i < stickerSets.length; i++) { set = stickerSets[i] if (set.pFlags.disabled || !set.pFlags.installed) { continue } acc = ((acc * 20261) + 0x80000000 + set.hash) % 0x80000000 } return acc } }) .service('AppInlineBotsManager', function (qSync, $q, $rootScope, toaster, Storage, ErrorService, MtpApiManager, AppMessagesManager, AppMessagesIDsManager, AppDocsManager, AppPhotosManager, AppGamesManager, RichTextProcessor, AppUsersManager, AppPeersManager, LocationParamsService, PeersSelectService, GeoLocationManager) { var inlineResults = {} return { resolveInlineMention: resolveInlineMention, getPopularBots: getPopularBots, sendInlineResult: sendInlineResult, getInlineResults: getInlineResults, regroupWrappedResults: regroupWrappedResults, switchToPM: switchToPM, checkSwitchReturn: checkSwitchReturn, switchInlineButtonClick: switchInlineButtonClick, callbackButtonClick: callbackButtonClick, gameButtonClick: gameButtonClick } function getPopularBots () { return Storage.get('inline_bots_popular').then(function (bots) { var result = [] var i, len var userID if (bots && bots.length) { var now = tsNow(true) for (i = 0, len = bots.length; i < len; i++) { if ((now - bots[i][3]) > 14 * 86400) { continue } userID = bots[i][0] if (!AppUsersManager.hasUser(userID)) { AppUsersManager.saveApiUser(bots[i][1]) } result.push({id: userID, rate: bots[i][2], date: bots[i][3]}) } } return result }) } function pushPopularBot (id) { getPopularBots().then(function (bots) { var exists = false var count = bots.length var result = [] for (var i = 0; i < count; i++) { if (bots[i].id == id) { exists = true bots[i].rate++ bots[i].date = tsNow(true) } var user = AppUsersManager.getUser(bots[i].id) result.push([bots[i].id, user, bots[i].rate, bots[i].date]) } if (exists) { result.sort(function (a, b) { return b[2] - a[2] }) } else { if (result.length > 15) { result = result.slice(0, 15) } result.push([id, AppUsersManager.getUser(id), 1, tsNow(true)]) } ConfigStorage.set({inline_bots_popular: result}) $rootScope.$broadcast('inline_bots_popular') }) } function resolveInlineMention (username) { return AppPeersManager.resolveUsername(username).then(function (peerID) { if (peerID > 0) { var bot = AppUsersManager.getUser(peerID) if (bot.pFlags.bot && bot.bot_inline_placeholder !== undefined) { var resolvedBot = { username: username, id: peerID, placeholder: bot.bot_inline_placeholder } if (bot.pFlags.bot_inline_geo && GeoLocationManager.isAvailable()) { return checkGeoLocationAccess(peerID).then(function () { return GeoLocationManager.getPosition().then(function (coords) { resolvedBot.geo = coords return qSync.when(resolvedBot) }) })['catch'](function () { return qSync.when(resolvedBot) }) } return qSync.when(resolvedBot) } } return $q.reject() }, function (error) { error.handled = true return $q.reject(error) }) } function getInlineResults (peerID, botID, query, geo, offset) { return MtpApiManager.invokeApi('messages.getInlineBotResults', { flags: 0 | (geo ? 1 : 0), bot: AppUsersManager.getUserInput(botID), peer: AppPeersManager.getInputPeerByID(peerID), query: query, geo_point: geo && {_: 'inputGeoPoint', lat: geo['lat'], long: geo['long']}, offset: offset }, {timeout: 1, stopTime: -1, noErrorBox: true}).then(function (botResults) { var queryID = botResults.query_id delete botResults._ delete botResults.flags delete botResults.query_id if (botResults.switch_pm) { botResults.switch_pm.rText = RichTextProcessor.wrapRichText(botResults.switch_pm.text, {noLinebreaks: true, noLinks: true}) } angular.forEach(botResults.results, function (result) { var qID = queryID + '_' + result.id result.qID = qID result.botID = botID result.rTitle = RichTextProcessor.wrapRichText(result.title, {noLinebreaks: true, noLinks: true}) result.rDescription = RichTextProcessor.wrapRichText(result.description, {noLinebreaks: true, noLinks: true}) result.initials = (result.url || result.title || result.type || '').substr(0, 1) if (result.document) { AppDocsManager.saveDoc(result.document) } if (result.photo) { AppPhotosManager.savePhoto(result.photo) } inlineResults[qID] = result }) return botResults }) } function regroupWrappedResults (results, rowW, rowH) { if (!results || !results[0] || ['photo', 'gif', 'sticker'].indexOf(results[0].type) == -1) { return } var ratios = [] angular.forEach(results, function (result) { var w var h, doc var photo if (result._ == 'botInlineMediaResult') { if (doc = result.document) { w = result.document.w h = result.document.h } else if (photo = result.photo) { var photoSize = (photo.sizes || [])[0] w = photoSize && photoSize.w h = photoSize && photoSize.h } }else { w = result.w h = result.h } if (!w || !h) { w = h = 1 } ratios.push(w / h) }) var rows = [] var curCnt = 0 var curW = 0 angular.forEach(ratios, function (ratio) { var w = ratio * rowH curW += w if (!curCnt || curCnt < 4 && curW < (rowW * 1.1)) { curCnt++ } else { rows.push(curCnt) curCnt = 1 curW = w } }) if (curCnt) { rows.push(curCnt) } var i = 0 var thumbs = [] var lastRowI = rows.length - 1 angular.forEach(rows, function (rowCnt, rowI) { var lastRow = rowI == lastRowI var curRatios = ratios.slice(i, i + rowCnt) var sumRatios = 0 angular.forEach(curRatios, function (ratio) { sumRatios += ratio }) angular.forEach(curRatios, function (ratio, j) { var thumbH = rowH var thumbW = rowW * ratio / sumRatios var realW = thumbH * ratio if (lastRow && thumbW > realW) { thumbW = realW } var result = results[i + j] result.thumbW = Math.floor(thumbW) - 2 result.thumbH = Math.floor(thumbH) - 2 }) i += rowCnt }) } function switchToPM (fromPeerID, botID, startParam) { var peerString = AppPeersManager.getPeerString(fromPeerID) var setHash = {} setHash['inline_switch_pm' + botID] = {peer: peerString, time: tsNow()} Storage.set(setHash) $rootScope.$broadcast('history_focus', {peerString: AppPeersManager.getPeerString(botID)}) AppMessagesManager.startBot(botID, 0, startParam) } function checkSwitchReturn (botID) { var bot = AppUsersManager.getUser(botID) if (!bot || !bot.pFlags.bot || !bot.bot_inline_placeholder) { return qSync.when(false) } var key = 'inline_switch_pm' + botID return Storage.get(key).then(function (peerData) { if (peerData) { Storage.remove(key) if (tsNow() - peerData.time < 3600000) { return peerData.peer } } return false }) } function switchInlineQuery (botID, toPeerString, query) { $rootScope.$broadcast('history_focus', { peerString: toPeerString, attachment: { _: 'inline_query', mention: '@' + AppUsersManager.getUser(botID).username, query: query } }) } function switchInlineButtonClick (id, button) { var message = AppMessagesManager.getMessage(id) var botID = message.viaBotID || message.fromID if (button.pFlags && button.pFlags.same_peer) { var peerID = AppMessagesManager.getMessagePeer(message) var toPeerString = AppPeersManager.getPeerString(peerID) switchInlineQuery(botID, toPeerString, button.query) return } return checkSwitchReturn(botID).then(function (retPeerString) { if (retPeerString) { return switchInlineQuery(botID, retPeerString, button.query) } PeersSelectService.selectPeer({ canSend: true }).then(function (toPeerString) { return switchInlineQuery(botID, toPeerString, button.query) }) }) } function callbackButtonClick (id, button) { var message = AppMessagesManager.getMessage(id) var botID = message.fromID var peerID = AppMessagesManager.getMessagePeer(message) return MtpApiManager.invokeApi('messages.getBotCallbackAnswer', { flags: 1, peer: AppPeersManager.getInputPeerByID(peerID), msg_id: AppMessagesIDsManager.getMessageLocalID(id), data: button.data }, {timeout: 1, stopTime: -1, noErrorBox: true}).then(function (callbackAnswer) { if (typeof callbackAnswer.message === 'string' && callbackAnswer.message.length) { showCallbackMessage(callbackAnswer.message, callbackAnswer.pFlags.alert) } else if (typeof callbackAnswer.url === 'string') { var url = RichTextProcessor.wrapUrl(callbackAnswer.url, true) LocationParamsService.openUrl(url) } }) } function gameButtonClick (id) { var message = AppMessagesManager.getMessage(id) var peerID = AppMessagesManager.getMessagePeer(message) return MtpApiManager.invokeApi('messages.getBotCallbackAnswer', { flags: 2, peer: AppPeersManager.getInputPeerByID(peerID), msg_id: AppMessagesIDsManager.getMessageLocalID(id) }, {timeout: 1, stopTime: -1, noErrorBox: true}).then(function (callbackAnswer) { if (typeof callbackAnswer.message === 'string' && callbackAnswer.message.length) { showCallbackMessage(callbackAnswer.message, callbackAnswer.pFlags.alert) } else if (typeof callbackAnswer.url === 'string') { AppGamesManager.openGame(message.media.game.id, id, callbackAnswer.url) } }) } function showCallbackMessage(message, isAlert) { if (typeof message != 'string' || !message.length) { return } var html = RichTextProcessor.wrapRichText(message, {noLinks: true, noLinebreaks: true}) if (isAlert) { ErrorService.show({ title_html: html, alert: true }) } else { toaster.pop({ type: 'info', body: html.valueOf(), bodyOutputType: 'trustedHtml', showCloseButton: false }) } } function sendInlineResult (peerID, qID, options) { var inlineResult = inlineResults[qID] if (inlineResult === undefined) { return false } pushPopularBot(inlineResult.botID) var splitted = qID.split('_') var queryID = splitted.shift() var resultID = splitted.join('_') options = options || {} options.viaBotID = inlineResult.botID options.queryID = queryID options.resultID = resultID if (inlineResult.send_message.reply_markup) { options.reply_markup = inlineResult.send_message.reply_markup } if (inlineResult.send_message._ == 'botInlineMessageText') { options.entities = inlineResult.send_message.entities AppMessagesManager.sendText(peerID, inlineResult.send_message.message, options) } else { var caption = '' var inputMedia = false switch (inlineResult.send_message._) { case 'botInlineMessageMediaAuto': caption = inlineResult.send_message.caption if (inlineResult._ == 'botInlineMediaResult') { var doc = inlineResult.document var photo = inlineResult.photo if (doc) { inputMedia = { _: 'inputMediaDocument', id: {_: 'inputDocument', id: doc.id, access_hash: doc.access_hash}, caption: caption } } else { inputMedia = { _: 'inputMediaPhoto', id: {_: 'inputPhoto', id: photo.id, access_hash: photo.access_hash}, caption: caption } } } break case 'botInlineMessageMediaGeo': inputMedia = { _: 'inputMediaGeoPoint', geo_point: { _: 'inputGeoPoint', 'lat': inlineResult.send_message.geo['lat'], 'long': inlineResult.send_message.geo['long'] } } break case 'botInlineMessageMediaVenue': inputMedia = { _: 'inputMediaVenue', geo_point: { _: 'inputGeoPoint', 'lat': inlineResult.send_message.geo['lat'], 'long': inlineResult.send_message.geo['long'] }, title: inlineResult.send_message.title, address: inlineResult.send_message.address, provider: inlineResult.send_message.provider, venue_id: inlineResult.send_message.venue_id } break case 'botInlineMessageMediaContact': inputMedia = { _: 'inputMediaContact', phone_number: inlineResult.send_message.phone_number, first_name: inlineResult.send_message.first_name, last_name: inlineResult.send_message.last_name } break } if (!inputMedia) { inputMedia = { _: 'messageMediaPending', type: inlineResult.type, file_name: inlineResult.title || inlineResult.content_url || inlineResult.url, size: 0, progress: {percent: 30, total: 0} } } AppMessagesManager.sendOther(peerID, inputMedia, options) } } function checkGeoLocationAccess (botID) { var key = 'bot_access_geo' + botID return Storage.get(key).then(function (geoAccess) { if (geoAccess && geoAccess.granted) { return true } return ErrorService.confirm({ type: 'BOT_ACCESS_GEO_INLINE' }).then(function () { var setHash = {} setHash[key] = {granted: true, time: tsNow()} Storage.set(setHash) return true }, function () { var setHash = {} setHash[key] = {denied: true, time: tsNow()} Storage.set(setHash) return $q.reject() }) }) } }) .service('ApiUpdatesManager', function ($rootScope, MtpNetworkerFactory, AppUsersManager, AppChatsManager, AppPeersManager, MtpApiManager) { var updatesState = { pendingPtsUpdates: [], pendingSeqUpdates: {}, syncPending: false, syncLoading: true } var channelStates = {} var myID = 0 MtpApiManager.getUserID().then(function (id) { myID = id }) function popPendingSeqUpdate () { var nextSeq = updatesState.seq + 1 var pendingUpdatesData = updatesState.pendingSeqUpdates[nextSeq] if (!pendingUpdatesData) { return false } var updates = pendingUpdatesData.updates var i var length for (var i = 0, length = updates.length; i < length; i++) { saveUpdate(updates[i]) } updatesState.seq = pendingUpdatesData.seq if (pendingUpdatesData.date && updatesState.date < pendingUpdatesData.date) { updatesState.date = pendingUpdatesData.date } delete updatesState.pendingSeqUpdates[nextSeq] if (!popPendingSeqUpdate() && updatesState.syncPending && updatesState.syncPending.seqAwaiting && updatesState.seq >= updatesState.syncPending.seqAwaiting) { if (!updatesState.syncPending.ptsAwaiting) { clearTimeout(updatesState.syncPending.timeout) updatesState.syncPending = false } else { delete updatesState.syncPending.seqAwaiting } } return true } function popPendingPtsUpdate (channelID) { var curState = channelID ? getChannelState(channelID) : updatesState if (!curState.pendingPtsUpdates.length) { return false } curState.pendingPtsUpdates.sort(function (a, b) { return a.pts - b.pts }) // console.log(dT(), 'pop update', channelID, curState.pendingPtsUpdates) var curPts = curState.pts var goodPts = false var goodIndex = false var update for (var i = 0, length = curState.pendingPtsUpdates.length; i < length; i++) { update = curState.pendingPtsUpdates[i] curPts += update.pts_count if (curPts >= update.pts) { goodPts = update.pts goodIndex = i } } if (!goodPts) { return false } console.log(dT(), 'pop pending pts updates', goodPts, curState.pendingPtsUpdates.slice(0, goodIndex + 1)) curState.pts = goodPts for (i = 0; i <= goodIndex; i++) { update = curState.pendingPtsUpdates[i] saveUpdate(update) } curState.pendingPtsUpdates.splice(0, goodIndex + 1) if (!curState.pendingPtsUpdates.length && curState.syncPending) { if (!curState.syncPending.seqAwaiting) { clearTimeout(curState.syncPending.timeout) curState.syncPending = false } else { delete curState.syncPending.ptsAwaiting } } return true } function forceGetDifference () { if (!updatesState.syncLoading) { getDifference() } } function processUpdateMessage (updateMessage, fromMTProto) { // return forceGetDifference() var processOpts = { date: updateMessage.date, seq: updateMessage.seq, seqStart: updateMessage.seq_start } switch (updateMessage._) { case 'updatesTooLong': case 'new_session_created': forceGetDifference() break case 'updateShort': processUpdate(updateMessage.update, processOpts) break case 'updateShortMessage': case 'updateShortChatMessage': var isOut = updateMessage.flags & 2 var fromID = updateMessage.from_id || (isOut ? myID : updateMessage.user_id) var toID = updateMessage.chat_id ? -updateMessage.chat_id : (isOut ? updateMessage.user_id : myID) processUpdate({ _: 'updateNewMessage', message: { _: 'message', flags: updateMessage.flags, pFlags: updateMessage.pFlags, id: updateMessage.id, from_id: fromID, to_id: AppPeersManager.getOutputPeer(toID), date: updateMessage.date, message: updateMessage.message, fwd_from: updateMessage.fwd_from, reply_to_msg_id: updateMessage.reply_to_msg_id, entities: updateMessage.entities }, pts: updateMessage.pts, pts_count: updateMessage.pts_count }, processOpts) break case 'updatesCombined': case 'updates': AppUsersManager.saveApiUsers(updateMessage.users) AppChatsManager.saveApiChats(updateMessage.chats) angular.forEach(updateMessage.updates, function (update) { processUpdate(update, processOpts) }) break default: console.warn(dT(), 'Unknown update message', updateMessage) } } function getDifference () { // console.trace(dT(), 'Get full diff') if (!updatesState.syncLoading) { updatesState.syncLoading = true updatesState.pendingSeqUpdates = {} updatesState.pendingPtsUpdates = [] } if (updatesState.syncPending) { clearTimeout(updatesState.syncPending.timeout) updatesState.syncPending = false } MtpApiManager.invokeApi('updates.getDifference', {pts: updatesState.pts, date: updatesState.date, qts: -1}, { timeout: 0x7fffffff }).then(function (differenceResult) { if (differenceResult._ == 'updates.differenceEmpty') { console.log(dT(), 'apply empty diff', differenceResult.seq) updatesState.date = differenceResult.date updatesState.seq = differenceResult.seq updatesState.syncLoading = false $rootScope.$broadcast('stateSynchronized') return false } AppUsersManager.saveApiUsers(differenceResult.users) AppChatsManager.saveApiChats(differenceResult.chats) // Should be first because of updateMessageID // console.log(dT(), 'applying', differenceResult.other_updates.length, 'other updates') var channelsUpdates = [] angular.forEach(differenceResult.other_updates, function (update) { switch (update._) { case 'updateChannelTooLong': case 'updateNewChannelMessage': case 'updateEditChannelMessage': processUpdate(update) return } saveUpdate(update) }) // console.log(dT(), 'applying', differenceResult.new_messages.length, 'new messages') angular.forEach(differenceResult.new_messages, function (apiMessage) { saveUpdate({ _: 'updateNewMessage', message: apiMessage, pts: updatesState.pts, pts_count: 0 }) }) var nextState = differenceResult.intermediate_state || differenceResult.state updatesState.seq = nextState.seq updatesState.pts = nextState.pts updatesState.date = nextState.date // console.log(dT(), 'apply diff', updatesState.seq, updatesState.pts) if (differenceResult._ == 'updates.differenceSlice') { getDifference() } else { // console.log(dT(), 'finished get diff') $rootScope.$broadcast('stateSynchronized') updatesState.syncLoading = false } }, function () { updatesState.syncLoading = false }) } function getChannelDifference (channelID) { var channelState = getChannelState(channelID) if (!channelState.syncLoading) { channelState.syncLoading = true channelState.pendingPtsUpdates = [] } if (channelState.syncPending) { clearTimeout(channelState.syncPending.timeout) channelState.syncPending = false } // console.log(dT(), 'Get channel diff', AppChatsManager.getChat(channelID), channelState.pts) MtpApiManager.invokeApi('updates.getChannelDifference', { channel: AppChatsManager.getChannelInput(channelID), filter: {_: 'channelMessagesFilterEmpty'}, pts: channelState.pts, limit: 30 }, {timeout: 0x7fffffff}).then(function (differenceResult) { // console.log(dT(), 'channel diff result', differenceResult) channelState.pts = differenceResult.pts if (differenceResult._ == 'updates.channelDifferenceEmpty') { console.log(dT(), 'apply channel empty diff', differenceResult) channelState.syncLoading = false $rootScope.$broadcast('stateSynchronized') return false } if (differenceResult._ == 'updates.channelDifferenceTooLong') { console.log(dT(), 'channel diff too long', differenceResult) channelState.syncLoading = false delete channelStates[channelID] saveUpdate({_: 'updateChannelReload', channel_id: channelID}) return false } AppUsersManager.saveApiUsers(differenceResult.users) AppChatsManager.saveApiChats(differenceResult.chats) // Should be first because of updateMessageID console.log(dT(), 'applying', differenceResult.other_updates.length, 'channel other updates') angular.forEach(differenceResult.other_updates, function (update) { saveUpdate(update) }) console.log(dT(), 'applying', differenceResult.new_messages.length, 'channel new messages') angular.forEach(differenceResult.new_messages, function (apiMessage) { saveUpdate({ _: 'updateNewChannelMessage', message: apiMessage, pts: channelState.pts, pts_count: 0 }) }) console.log(dT(), 'apply channel diff', channelState.pts) if (differenceResult._ == 'updates.channelDifference' && !differenceResult.pFlags['final']) { getChannelDifference(channelID) } else { console.log(dT(), 'finished channel get diff') $rootScope.$broadcast('stateSynchronized') channelState.syncLoading = false } }, function () { channelState.syncLoading = false }) } function addChannelState (channelID, pts) { if (!pts) { throw new Error('Add channel state without pts ' + channelID) } if (channelStates[channelID] === undefined) { channelStates[channelID] = { pts: pts, pendingPtsUpdates: [], syncPending: false, syncLoading: false } return true } return false } function getChannelState (channelID, pts) { if (channelStates[channelID] === undefined) { addChannelState(channelID, pts) } return channelStates[channelID] } function processUpdate (update, options) { options = options || {} var channelID = false switch (update._) { case 'updateNewChannelMessage': case 'updateEditChannelMessage': channelID = -AppPeersManager.getPeerID(update.message.to_id) break case 'updateDeleteChannelMessages': channelID = update.channel_id break case 'updateChannelTooLong': channelID = update.channel_id if (channelStates[channelID] === undefined) { return false } break } var curState = channelID ? getChannelState(channelID, update.pts) : updatesState // console.log(dT(), 'process', channelID, curState.pts, update) if (curState.syncLoading) { return false } if (update._ == 'updateChannelTooLong') { if (!curState.lastPtsUpdateTime || curState.lastPtsUpdateTime < tsNow() - 10000) { // console.trace(dT(), 'channel too long, get diff', channelID, update) getChannelDifference(channelID) } return false } if (update._ == 'updateNewMessage' || update._ == 'updateEditMessage' || update._ == 'updateNewChannelMessage' || update._ == 'updateEditChannelMessage') { var message = update.message var toPeerID = AppPeersManager.getPeerID(message.to_id) var fwdHeader = message.fwd_from || {} var reason = false if (message.from_id && !AppUsersManager.hasUser(message.from_id, message.pFlags.post/* || channelID*/) && (reason = 'author') || fwdHeader.from_id && !AppUsersManager.hasUser(fwdHeader.from_id, !!fwdHeader.channel_id) && (reason = 'fwdAuthor') || fwdHeader.channel_id && !AppChatsManager.hasChat(fwdHeader.channel_id, true) && (reason = 'fwdChannel') || toPeerID > 0 && !AppUsersManager.hasUser(toPeerID) && (reason = 'toPeer User') || toPeerID < 0 && !AppChatsManager.hasChat(-toPeerID) && (reason = 'toPeer Chat')) { console.warn(dT(), 'Not enough data for message update', toPeerID, reason, message) if (channelID && AppChatsManager.hasChat(channelID)) { getChannelDifference(channelID) } else { forceGetDifference() } return false } } else if (channelID && !AppChatsManager.hasChat(channelID)) { // console.log(dT(), 'skip update, missing channel', channelID, update) return false } var popPts var popSeq if (update.pts) { var newPts = curState.pts + (update.pts_count || 0) if (newPts < update.pts) { console.warn(dT(), 'Pts hole', curState, update, channelID && AppChatsManager.getChat(channelID)) curState.pendingPtsUpdates.push(update) if (!curState.syncPending) { curState.syncPending = { timeout: setTimeout(function () { if (channelID) { getChannelDifference(channelID) } else { getDifference() } }, 5000) } } curState.syncPending.ptsAwaiting = true return false } if (update.pts > curState.pts) { curState.pts = update.pts popPts = true curState.lastPtsUpdateTime = tsNow() } else if (update.pts_count) { // console.warn(dT(), 'Duplicate update', update) return false } if (channelID && options.date && updatesState.date < options.date) { updatesState.date = options.date } } else if (!channelID && options.seq > 0) { var seq = options.seq var seqStart = options.seqStart || seq if (seqStart != curState.seq + 1) { if (seqStart > curState.seq) { console.warn(dT(), 'Seq hole', curState, curState.syncPending && curState.syncPending.seqAwaiting) if (curState.pendingSeqUpdates[seqStart] === undefined) { curState.pendingSeqUpdates[seqStart] = {seq: seq, date: options.date, updates: []} } curState.pendingSeqUpdates[seqStart].updates.push(update) if (!curState.syncPending) { curState.syncPending = { timeout: setTimeout(function () { getDifference() }, 5000) } } if (!curState.syncPending.seqAwaiting || curState.syncPending.seqAwaiting < seqStart) { curState.syncPending.seqAwaiting = seqStart } return false } } if (curState.seq != seq) { curState.seq = seq if (options.date && curState.date < options.date) { curState.date = options.date } popSeq = true } } saveUpdate(update) if (popPts) { popPendingPtsUpdate(channelID) } else if (popSeq) { popPendingSeqUpdate() } } function saveUpdate (update) { $rootScope.$broadcast('apiUpdate', update) } function attach () { MtpNetworkerFactory.setUpdatesProcessor(processUpdateMessage) MtpApiManager.invokeApi('updates.getState', {}, {noErrorBox: true}).then(function (stateResult) { updatesState.seq = stateResult.seq updatesState.pts = stateResult.pts updatesState.date = stateResult.date setTimeout(function () { updatesState.syncLoading = false }, 1000) // updatesState.seq = 1 // updatesState.pts = stateResult.pts - 5000 // updatesState.date = 1 // getDifference() }) } return { processUpdateMessage: processUpdateMessage, addChannelState: addChannelState, attach: attach } }) .service('StatusManager', function ($timeout, $rootScope, MtpApiManager, AppUsersManager, IdleManager) { var toPromise var lastOnlineUpdated = 0 var started = false var myID = 0 var myOtherDeviceActive = false MtpApiManager.getUserID().then(function (id) { myID = id }) $rootScope.$on('apiUpdate', function (e, update) { if (update._ == 'updateUserStatus' && update.user_id == myID) { myOtherDeviceActive = tsNow() + (update.status._ == 'userStatusOnline' ? 300000 : 0) } }) return { start: start, isOtherDeviceActive: isOtherDeviceActive } function start () { if (!started) { started = true $rootScope.$watch('idle.isIDLE', checkIDLE) $rootScope.$watch('offline', checkIDLE) } } function sendUpdateStatusReq (offline) { var date = tsNow() if (offline && !lastOnlineUpdated || !offline && (date - lastOnlineUpdated) < 50000 || $rootScope.offline) { return } lastOnlineUpdated = offline ? 0 : date AppUsersManager.setUserStatus(myID, offline) return MtpApiManager.invokeApi('account.updateStatus', { offline: offline }, {noErrorBox: true}) } function checkIDLE () { toPromise && $timeout.cancel(toPromise) if ($rootScope.idle.isIDLE) { toPromise = $timeout(function () { sendUpdateStatusReq(true) }, 5000) } else { sendUpdateStatusReq(false) toPromise = $timeout(checkIDLE, 60000) } } function isOtherDeviceActive () { if (!myOtherDeviceActive) { return false } if (tsNow() > myOtherDeviceActive) { myOtherDeviceActive = false return false } return true } }) .service('NotificationsManager', function ($rootScope, $window, $interval, $q, $modal, _, toaster, MtpApiManager, AppPeersManager, AppChatsManager, AppUsersManager, IdleManager, Storage, AppRuntimeManager, FileManager, WebPushApiManager) { navigator.vibrate = navigator.vibrate || navigator.mozVibrate || navigator.webkitVibrate var notificationsMsSiteMode = false try { if (window.external && window.external.msIsSiteMode()) { notificationsMsSiteMode = true } } catch (e) {} var notificationsUiSupport = notificationsMsSiteMode || ('Notification' in window) || ('mozNotification' in navigator) var notificationsShown = {} var notificationIndex = 0 var notificationsCount = 0 var soundsPlayed = {} var vibrateSupport = !!navigator.vibrate var nextSoundAt = false var prevSoundVolume = false var peerSettings = {} var faviconEl = $('link[rel="icon"]:first')[0] var langNotificationsPluralize = _.pluralize('page_title_pluralize_notifications') var titleBackup = document.title var titleChanged = false var titlePromise var prevFavicon var stopped = false var settings = {} $rootScope.$watch('idle.deactivated', function (newVal) { if (newVal) { stop() } }) $rootScope.$watch('idle.isIDLE', function (newVal) { if (stopped) { return } if (!newVal) { notificationsClear() } if (!Config.Navigator.mobile) { $interval.cancel(titlePromise) if (!newVal) { titleChanged = false document.title = titleBackup setFavicon() } else { titleBackup = document.title titlePromise = $interval(function () { if (titleChanged || !notificationsCount) { titleChanged = false document.title = titleBackup setFavicon() } else { titleChanged = true document.title = langNotificationsPluralize(notificationsCount) setFavicon('favicon_unread.ico') } }, 1000) } } }) $rootScope.$on('apiUpdate', function (e, update) { // console.log('on apiUpdate', update) switch (update._) { case 'updateNotifySettings': if (update.peer._ == 'notifyPeer') { var peerID = AppPeersManager.getPeerID(update.peer.peer) savePeerSettings(peerID, update.notify_settings) } break } }) var registeredDevice = false var pushInited = false $rootScope.$on('push_init', function (e, tokenData) { pushInited = true if (!settings.nodesktop && !settings.nopush) { if (tokenData) { registerDevice(tokenData) } else { WebPushApiManager.subscribe() } } else { unregisterDevice(tokenData) } }) $rootScope.$on('push_subscribe', function (e, tokenData) { registerDevice(tokenData) }) $rootScope.$on('push_unsubscribe', function (e, tokenData) { unregisterDevice(tokenData) }) var topMessagesDeferred = $q.defer() var unregisterTopMsgs = $rootScope.$on('dialogs_multiupdate', function () { unregisterTopMsgs() topMessagesDeferred.resolve() }) var topMessagesPromise = topMessagesDeferred.promise $rootScope.$on('push_notification_click', function (e, notificationData) { if (notificationData.action == 'push_settings') { topMessagesPromise.then(function () { $modal.open({ templateUrl: templateUrl('settings_modal'), controller: 'SettingsModalController', windowClass: 'settings_modal_window mobile_modal', backdrop: 'single' }) }) return } if (notificationData.action == 'mute1d') { MtpApiManager.invokeApi('account.updateDeviceLocked', function () { period: 86400 }).then(function () { var toastData = toaster.pop({ type: 'info', body: _('push_action_mute1d_success'), bodyOutputType: 'trustedHtml', clickHandler: function () { toaster.clear(toastData) }, showCloseButton: false }) }) return } var peerID = notificationData.custom && notificationData.custom.peerID console.log('click', notificationData, peerID) if (peerID) { topMessagesPromise.then(function () { if (notificationData.custom.channel_id && !AppChatsManager.hasChat(notificationData.custom.channel_id)) { return } if (peerID > 0 && !AppUsersManager.hasUser(peerID)) { return } $rootScope.$broadcast('history_focus', { peerString: AppPeersManager.getPeerString(peerID) }) }) } }) return { start: start, notify: notify, cancel: notificationCancel, clear: notificationsClear, soundReset: notificationSoundReset, getPeerSettings: getPeerSettings, getPeerMuted: getPeerMuted, savePeerSettings: savePeerSettings, updatePeerSettings: updatePeerSettings, updateNotifySettings: updateNotifySettings, getNotifySettings: getNotifySettings, getVibrateSupport: getVibrateSupport, testSound: playSound } function updateNotifySettings () { Storage.get('notify_nodesktop', 'notify_volume', 'notify_novibrate', 'notify_nopreview', 'notify_nopush').then(function (updSettings) { settings.nodesktop = updSettings[0] settings.volume = updSettings[1] === false ? 0.5 : updSettings[1] settings.novibrate = updSettings[2] settings.nopreview = updSettings[3] settings.nopush = updSettings[4] if (pushInited) { var needPush = !settings.nopush && !settings.nodesktop && WebPushApiManager.isAvailable || false var hasPush = registeredDevice !== false if (needPush != hasPush) { if (needPush) { WebPushApiManager.subscribe() } else { WebPushApiManager.unsubscribe() } } } WebPushApiManager.setSettings(settings) }) } function getNotifySettings () { return settings } function getPeerSettings (peerID) { if (peerSettings[peerID] !== undefined) { return peerSettings[peerID] } return peerSettings[peerID] = MtpApiManager.invokeApi('account.getNotifySettings', { peer: { _: 'inputNotifyPeer', peer: AppPeersManager.getInputPeerByID(peerID) } }) } function setFavicon (href) { href = href || 'favicon.ico' if (prevFavicon === href) { return } var link = document.createElement('link') link.rel = 'shortcut icon' link.type = 'image/x-icon' link.href = href faviconEl.parentNode.replaceChild(link, faviconEl) faviconEl = link prevFavicon = href } function savePeerSettings (peerID, settings) { // console.trace(dT(), 'peer settings', peerID, settings) peerSettings[peerID] = $q.when(settings) $rootScope.$broadcast('notify_settings', {peerID: peerID}) } function updatePeerSettings (peerID, settings) { savePeerSettings(peerID, settings) var inputSettings = angular.copy(settings) inputSettings._ = 'inputPeerNotifySettings' return MtpApiManager.invokeApi('account.updateNotifySettings', { peer: { _: 'inputNotifyPeer', peer: AppPeersManager.getInputPeerByID(peerID) }, settings: inputSettings }) } function getPeerMuted (peerID) { return getPeerSettings(peerID).then(function (peerNotifySettings) { return peerNotifySettings._ == 'peerNotifySettings' && peerNotifySettings.mute_until * 1000 > tsNow() }) } function start () { updateNotifySettings() $rootScope.$on('settings_changed', updateNotifySettings) WebPushApiManager.start() if (!notificationsUiSupport) { return false } if ('Notification' in window && Notification.permission !== 'granted' && Notification.permission !== 'denied') { $($window).on('click', requestPermission) } try { if ('onbeforeunload' in window) { $($window).on('beforeunload', notificationsClear) } } catch (e) {} } function stop () { notificationsClear() $interval.cancel(titlePromise) setFavicon() stopped = true } function requestPermission () { Notification.requestPermission() $($window).off('click', requestPermission) } function notify (data) { console.log('notify', data, $rootScope.idle.isIDLE, notificationsUiSupport, stopped) if (stopped) { return } // FFOS Notification blob src bug workaround if (Config.Navigator.ffos && !Config.Navigator.ffos2p) { data.image = 'https://telegram.org/img/t_logo.png' } else if (data.image && !angular.isString(data.image)) { if (Config.Navigator.ffos2p) { FileManager.getDataUrl(data.image, 'image/jpeg').then(function (url) { data.image = url notify(data) }) return false } else { data.image = FileManager.getUrl(data.image, 'image/jpeg') } } else if (!data.image) { data.image = 'img/icons/icon60.png' } // console.log('notify image', data.image) notificationsCount++ var now = tsNow() if (settings.volume > 0 && ( !data.tag || !soundsPlayed[data.tag] || now > soundsPlayed[data.tag] + 60000 ) ) { playSound(settings.volume) soundsPlayed[data.tag] = now } if (!notificationsUiSupport || 'Notification' in window && Notification.permission !== 'granted') { return false } if (settings.nodesktop) { if (vibrateSupport && !settings.novibrate) { navigator.vibrate([200, 100, 200]) return } return } var idx = ++notificationIndex var key = data.key || 'k' + idx var notification if ('Notification' in window) { try { if (data.tag) { angular.forEach(notificationsShown, function (notification) { if (notification && notification.tag == data.tag) { notification.hidden = true } }) } notification = new Notification(data.title, { icon: data.image || '', body: data.message || '', tag: data.tag || '', silent: data.silent || false }) } catch (e) { notificationsUiSupport = false WebPushApiManager.setLocalNotificationsDisabled() return } } else if ('mozNotification' in navigator) { notification = navigator.mozNotification.createNotification(data.title, data.message || '', data.image || '') } else if (notificationsMsSiteMode) { window.external.msSiteModeClearIconOverlay() window.external.msSiteModeSetIconOverlay('img/icons/icon16.png', data.title) window.external.msSiteModeActivate() notification = { index: idx } } else { return } notification.onclick = function () { notification.close() AppRuntimeManager.focus() notificationsClear() if (data.onclick) { data.onclick() } } notification.onclose = function () { if (!notification.hidden) { delete notificationsShown[key] notificationsClear() } } if (notification.show) { notification.show() } notificationsShown[key] = notification if (!Config.Navigator.mobile) { setTimeout(function () { notificationHide(key) }, 8000) } } function playSound (volume) { var now = tsNow() if (nextSoundAt && now < nextSoundAt && prevSoundVolume == volume) { return } nextSoundAt = now + 1000 prevSoundVolume = volume var filename = 'img/sound_a.mp3' var obj = $('#notify_sound').html('') obj.find('audio')[0].volume = volume } function notificationCancel (key) { var notification = notificationsShown[key] if (notification) { if (notificationsCount > 0) { notificationsCount-- } try { if (notification.close) { notification.hidden = true notification.close() } else if (notificationsMsSiteMode && notification.index == notificationIndex) { window.external.msSiteModeClearIconOverlay() } } catch (e) {} delete notificationsCount[key] } } function notificationHide (key) { var notification = notificationsShown[key] if (notification) { try { if (notification.close) { notification.hidden = true notification.close() } } catch (e) {} delete notificationsCount[key] } } function notificationSoundReset (tag) { delete soundsPlayed[tag] } function notificationsClear () { if (notificationsMsSiteMode) { window.external.msSiteModeClearIconOverlay() } else { angular.forEach(notificationsShown, function (notification) { try { if (notification.close) { notification.close() } } catch (e) {} }) } notificationsShown = {} notificationsCount = 0 WebPushApiManager.hidePushNotifications() } function registerDevice (tokenData) { if (registeredDevice && angular.equals(registeredDevice, tokenData)) { return false } MtpApiManager.invokeApi('account.registerDevice', { token_type: tokenData.tokenType, token: tokenData.tokenValue, other_uids: [] }).then(function () { registeredDevice = tokenData }, function (error) { error.handled = true }) } function unregisterDevice (tokenData) { if (!registeredDevice) { return false } MtpApiManager.invokeApi('account.unregisterDevice', { token_type: tokenData.tokenType, token: tokenData.tokenValue, other_uids: [] }).then(function () { registeredDevice = false }, function (error) { error.handled = true }) } function getVibrateSupport () { return vibrateSupport } }) .service('PasswordManager', function ($timeout, $q, $rootScope, MtpApiManager, CryptoWorker, MtpSecureRandom) { return { check: check, getState: getState, requestRecovery: requestRecovery, recover: recover, updateSettings: updateSettings } function getState (options) { return MtpApiManager.invokeApi('account.getPassword', {}, options).then(function (result) { return result }) } function updateSettings (state, settings) { var currentHashPromise var newHashPromise var params = { new_settings: { _: 'account.passwordInputSettings', flags: 0, hint: settings.hint || '' } } if (typeof settings.cur_password === 'string' && settings.cur_password.length > 0) { currentHashPromise = makePasswordHash(state.current_salt, settings.cur_password) } else { currentHashPromise = $q.when([]) } if (typeof settings.new_password === 'string' && settings.new_password.length > 0) { var saltRandom = new Array(8) var newSalt = bufferConcat(state.new_salt, saltRandom) MtpSecureRandom.nextBytes(saltRandom) newHashPromise = makePasswordHash(newSalt, settings.new_password) params.new_settings.new_salt = newSalt params.new_settings.flags |= 1 } else { if (typeof settings.new_password === 'string') { params.new_settings.flags |= 1 params.new_settings.new_salt = [] } newHashPromise = $q.when([]) } if (typeof settings.email === 'string') { params.new_settings.flags |= 2 params.new_settings.email = settings.email || '' } return $q.all([currentHashPromise, newHashPromise]).then(function (hashes) { params.current_password_hash = hashes[0] params.new_settings.new_password_hash = hashes[1] return MtpApiManager.invokeApi('account.updatePasswordSettings', params) }) } function check (state, password, options) { return makePasswordHash(state.current_salt, password).then(function (passwordHash) { return MtpApiManager.invokeApi('auth.checkPassword', { password_hash: passwordHash }, options) }) } function requestRecovery (state, options) { return MtpApiManager.invokeApi('auth.requestPasswordRecovery', {}, options) } function recover (code, options) { return MtpApiManager.invokeApi('auth.recoverPassword', { code: code }, options) } function makePasswordHash (salt, password) { var passwordUTF8 = unescape(encodeURIComponent(password)) var buffer = new ArrayBuffer(passwordUTF8.length) var byteView = new Uint8Array(buffer) for (var i = 0, len = passwordUTF8.length; i < len; i++) { byteView[i] = passwordUTF8.charCodeAt(i) } buffer = bufferConcat(bufferConcat(salt, byteView), salt) return CryptoWorker.sha256Hash(buffer) } }) .service('ErrorService', function ($rootScope, $modal, $window) { var shownBoxes = 0 function show (params, options) { if (shownBoxes >= 1) { console.log('Skip error box, too many open', shownBoxes, params, options) return false } options = options || {} var scope = $rootScope.$new() angular.extend(scope, params) shownBoxes++ var modal = $modal.open({ templateUrl: templateUrl('error_modal'), scope: scope, windowClass: options.windowClass || 'error_modal_window' }) modal.result['finally'](function () { shownBoxes-- }) return modal } function alert (title, description) { return show({ title: title, description: description }) } function confirm (params, options, data) { options = options || {} data = data || {} var scope = $rootScope.$new() angular.extend(scope, params) angular.extend(scope, { data: data }) var modal = $modal.open({ templateUrl: templateUrl('confirm_modal'), scope: scope, windowClass: options.windowClass || 'confirm_modal_window' }) return modal.result } $window.safeConfirm = function (params, callback) { if (typeof params === 'string') { params = {message: params} } confirm(params).then(function (result) { callback(result || true) }, function () { callback(false) }) } return { show: show, alert: alert, confirm: confirm } }) .service('PeersSelectService', function ($rootScope, $modal) { function selectPeer (options) { var scope = $rootScope.$new() scope.multiSelect = false scope.noMessages = true scope.forPeerSelect = true if (options) { angular.extend(scope, options) } return $modal.open({ templateUrl: templateUrl('peer_select'), controller: 'PeerSelectController', scope: scope, windowClass: 'peer_select_window mobile_modal', backdrop: 'single' }).result } function selectPeers (options) { if (Config.Mobile) { return selectPeer(options).then(function (peerString) { return [peerString] }) } var scope = $rootScope.$new() scope.multiSelect = true scope.noMessages = true scope.forPeerSelect = true if (options) { angular.extend(scope, options) } return $modal.open({ templateUrl: templateUrl('peer_select'), controller: 'PeerSelectController', scope: scope, windowClass: 'peer_select_window mobile_modal', backdrop: 'single' }).result } return { selectPeer: selectPeer, selectPeers: selectPeers } }) .service('ContactsSelectService', function ($rootScope, $modal) { function select (multiSelect, options) { options = options || {} var scope = $rootScope.$new() scope.multiSelect = multiSelect angular.extend(scope, options) if (!scope.action && multiSelect) { scope.action = 'select' } return $modal.open({ templateUrl: templateUrl('contacts_modal'), controller: 'ContactsModalController', scope: scope, windowClass: 'contacts_modal_window mobile_modal', backdrop: 'single' }).result } return { selectContacts: function (options) { return select(true, options) }, selectContact: function (options) { return select(false, options) } } }) .service('ChangelogNotifyService', function (Storage, $rootScope, $modal, $timeout, MtpApiManager, ApiUpdatesManager) { var checked = false function checkUpdate () { if (checked) { return } checked = true MtpApiManager.getUserID().then(function (userID) { if (!userID) { return } $timeout(function () { Storage.get('last_version').then(function (lastVersion) { if (lastVersion != Config.App.version) { if (!lastVersion) { Storage.set({last_version: Config.App.version}) } else { MtpApiManager.invokeApi('help.getAppChangelog', { prev_app_version: lastVersion }, { noErrorBox: true, }).then(function (updates) { if (updates._ == 'updates' && !updates.updates.length) { return false } ApiUpdatesManager.processUpdateMessage(updates) Storage.set({last_version: Config.App.version}) }) } } }) }, 5000) }) } function showChangelog (lastVersion) { var $scope = $rootScope.$new() $scope.lastVersion = lastVersion $modal.open({ controller: 'ChangelogModalController', templateUrl: templateUrl('changelog_modal'), scope: $scope, windowClass: 'changelog_modal_window mobile_modal' }) } return { checkUpdate: checkUpdate, showChangelog: showChangelog } }) .service('HttpsMigrateService', function (ErrorService, Storage) { var started = false function check () { Storage.get('https_dismiss').then(function (ts) { if (!ts || tsNow() > ts + 43200000) { ErrorService.confirm({ type: 'MIGRATE_TO_HTTPS' }).then(function () { var popup try { popup = window.open('https://web.telegram.org', '_blank') } catch (e) {} if (!popup) { location.href = 'https://web.telegram.org' } }, function () { Storage.set({https_dismiss: tsNow()}) }) } }) } function start () { if (started || location.protocol != 'http:' || Config.Modes.http || Config.App.domains.indexOf(location.hostname) == -1) { return } started = true setTimeout(check, 120000) } return { start: start, check: check } }) .service('LayoutSwitchService', function (ErrorService, Storage, AppRuntimeManager, $window) { var started = false var confirmShown = false function switchLayout (mobile) { ConfigStorage.noPrefix() Storage.set({ layout_selected: mobile ? 'mobile' : 'desktop', layout_width: $(window).width() }).then(function () { AppRuntimeManager.reload() }) } function layoutCheck (e) { if (confirmShown) { return } var width = $(window).width() var newMobile = width < 600 if (!width || !e && (Config.Navigator.mobile ? width <= 800 : newMobile)) { return } if (newMobile != Config.Mobile) { ConfigStorage.noPrefix() Storage.get('layout_width').then(function (confirmedWidth) { if (width == confirmedWidth) { return false } confirmShown = true ErrorService.confirm({ type: newMobile ? 'SWITCH_MOBILE_VERSION' : 'SWITCH_DESKTOP_VERSION' }).then(function () { switchLayout(newMobile) }, function () { ConfigStorage.noPrefix() Storage.set({layout_width: width}) confirmShown = false }) }) } } function start () { if (started || Config.Navigator.mobile) { return } started = true layoutCheck() $($window).on('resize', layoutCheck) } return { start: start, switchLayout: switchLayout } }) .service('TelegramMeWebService', function (Storage) { var disabled = Config.Modes.test || Config.App.domains.indexOf(location.hostname) == -1 || location.protocol != 'http:' && location.protocol != 'https:' || location.protocol == 'https:' && location.hostname != 'web.telegram.org' function sendAsyncRequest (canRedirect) { if (disabled) { return false } Storage.get('tgme_sync').then(function (curValue) { var ts = tsNow(true) if (canRedirect && curValue && curValue.canRedirect == canRedirect && curValue.ts + 86400 > ts) { return false } Storage.set({tgme_sync: {canRedirect: canRedirect, ts: ts}}) var script1 = $('