/*! * Webogram v0.5.4 - messaging web application for MTProto * https://github.com/zhukov/webogram * Copyright (C) 2014 Igor Zhukov * https://github.com/zhukov/webogram/blob/master/LICENSE */ angular.module('izhukov.utils', []) .provider('Storage', function () { this.setPrefix = function (newPrefix) { ConfigStorage.prefix(newPrefix) } this.$get = ['$q', function ($q) { var methods = {} angular.forEach(['get', 'set', 'remove', 'clear'], function (methodName) { methods[methodName] = function () { var deferred = $q.defer() var args = Array.prototype.slice.call(arguments) args.push(function (result) { deferred.resolve(result) }) ConfigStorage[methodName].apply(ConfigStorage, args) return deferred.promise } }) methods.noPrefix = function () { ConfigStorage.noPrefix() } return methods }] }) .service('qSync', function () { return { when: function (result) { return {then: function (cb) { return cb(result) }} }, reject: function (result) { return {then: function (cb, badcb) { if (badcb) { return badcb(result) } }} } } }) .service('FileManager', function ($window, $q, $timeout, qSync) { $window.URL = $window.URL || $window.webkitURL $window.BlobBuilder = $window.BlobBuilder || $window.WebKitBlobBuilder || $window.MozBlobBuilder var buggyUnknownBlob = navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1 var blobSupported = true try { blobConstruct([], '') } catch (e) { blobSupported = false } function isBlobAvailable () { return blobSupported } function fileCopyTo (fromFileEntry, toFileEntry) { return getFileWriter(toFileEntry).then(function (fileWriter) { return fileWriteData(fileWriter, fromFileEntry).then(function () { return fileWriter }, function (error) { return $q.reject(error) fileWriter.truncate(0) }) }) } function fileWriteData (fileWriter, bytes) { var deferred = $q.defer() fileWriter.onwriteend = function (e) { deferred.resolve() } fileWriter.onerror = function (e) { deferred.reject(e) } if (bytes.file) { bytes.file(function (file) { fileWriter.write(file) }, function (error) { deferred.reject(error) }) } else if (bytes instanceof Blob) { // is file bytes fileWriter.write(bytes) }else { try { var blob = blobConstruct([bytesToArrayBuffer(bytes)]) fileWriter.write(blob) } catch (e) { deferred.reject(e) } } return deferred.promise } function chooseSaveFile (fileName, ext, mimeType) { if (!$window.chrome || !chrome.fileSystem || !chrome.fileSystem.chooseEntry) { return qSync.reject() } var deferred = $q.defer() chrome.fileSystem.chooseEntry({ type: 'saveFile', suggestedName: fileName, accepts: [{ mimeTypes: [mimeType], extensions: [ext] }] }, function (writableFileEntry) { deferred.resolve(writableFileEntry) }) return deferred.promise } function getFileWriter (fileEntry) { var deferred = $q.defer() fileEntry.createWriter(function (fileWriter) { deferred.resolve(fileWriter) }, function (error) { deferred.reject(error) }) return deferred.promise } function getFakeFileWriter (mimeType, saveFileCallback) { var blobParts = [] var fakeFileWriter = { write: function (blob) { if (!blobSupported) { if (fakeFileWriter.onerror) { fakeFileWriter.onerror(new Error('Blob not supported by browser')) } return false } blobParts.push(blob) setZeroTimeout(function () { if (fakeFileWriter.onwriteend) { fakeFileWriter.onwriteend() } }) }, truncate: function () { blobParts = [] }, finalize: function () { var blob = blobConstruct(blobParts, mimeType) if (saveFileCallback) { saveFileCallback(blob) } return blob } } return fakeFileWriter } function getUrl (fileData, mimeType) { // console.log(dT(), 'get url', fileData, mimeType, fileData.toURL !== undefined, fileData instanceof Blob) if (fileData.toURL !== undefined) { return fileData.toURL(mimeType) } if (fileData instanceof Blob) { return URL.createObjectURL(fileData) } return 'data:' + mimeType + ';base64,' + bytesToBase64(fileData) } function getByteArray (fileData) { if (fileData instanceof Blob) { var deferred = $q.defer() try { var reader = new FileReader() reader.onloadend = function (e) { deferred.resolve(new Uint8Array(e.target.result)) } reader.onerror = function (e) { deferred.reject(e) } reader.readAsArrayBuffer(fileData) return deferred.promise } catch (e) { return $q.reject(e) } } else if (fileData.file) { var deferred = $q.defer() fileData.file(function (blob) { getByteArray(blob).then(function (result) { deferred.resolve(result) }, function (error) { deferred.reject(error) }) }, function (error) { deferred.reject(error) }) return deferred.promise } return $q.when(fileData) } function getDataUrl (blob) { var deferred try { var reader = new FileReader() reader.onloadend = function () { deferred.resolve(reader.result) } reader.readAsDataURL(blob) } catch (e) { return $q.reject(e) } deferred = $q.defer() return deferred.promise } function getFileCorrectUrl (blob, mimeType) { if (buggyUnknownBlob && blob instanceof Blob) { var mimeType = blob.type || blob.mimeType || mimeType || '' if (!mimeType.match(/image\/(jpeg|gif|png|bmp)|video\/quicktime/)) { return getDataUrl(blob) } } return qSync.when(getUrl(blob, mimeType)) } function downloadFile (blob, mimeType, fileName) { if (window.navigator && navigator.msSaveBlob !== undefined) { window.navigator.msSaveBlob(blob, fileName) return false } if (window.navigator && navigator.getDeviceStorage) { var storageName = 'sdcard' var subdir = 'telegram/' switch (mimeType.split('/')[0]) { case 'video': storageName = 'videos' break case 'audio': storageName = 'music' break case 'image': storageName = 'pictures' break } var deviceStorage = navigator.getDeviceStorage(storageName) var request = deviceStorage.addNamed(blob, subdir + fileName) request.onsuccess = function () { console.log('Device storage save result', this.result) } request.onerror = function () {} return } var popup = false if (window.safari) { popup = window.open() } getFileCorrectUrl(blob, mimeType).then(function (url) { if (popup) { try { popup.location.href = url return } catch (e) {} } var anchor = document.createElementNS('http://www.w3.org/1999/xhtml', 'a') anchor.href = url anchor.target = '_blank' anchor.download = fileName if (anchor.dataset) { anchor.dataset.downloadurl = ['video/quicktime', fileName, url].join(':') } $(anchor).css({position: 'absolute', top: 1, left: 1}).appendTo('body') try { var clickEvent = document.createEvent('MouseEvents') clickEvent.initMouseEvent( 'click', true, false, window, 0, 0, 0, 0, 0 , false, false, false, false, 0, null ) anchor.dispatchEvent(clickEvent) } catch (e) { console.error('Download click error', e) try { anchor[0].click() } catch (e) { window.open(url, '_blank') } } $timeout(function () { $(anchor).remove() }, 100) }) } return { isAvailable: isBlobAvailable, copy: fileCopyTo, write: fileWriteData, getFileWriter: getFileWriter, getFakeFileWriter: getFakeFileWriter, chooseSave: chooseSaveFile, getUrl: getUrl, getDataUrl: getDataUrl, getByteArray: getByteArray, getFileCorrectUrl: getFileCorrectUrl, download: downloadFile } }) .service('IdbFileStorage', function ($q, $window, FileManager) { $window.indexedDB = $window.indexedDB || $window.webkitIndexedDB || $window.mozIndexedDB || $window.OIndexedDB || $window.msIndexedDB $window.IDBTransaction = $window.IDBTransaction || $window.webkitIDBTransaction || $window.OIDBTransaction || $window.msIDBTransaction var dbName = 'cachedFiles' var dbStoreName = 'files' var dbVersion = 2 var openDbPromise var storageIsAvailable = $window.indexedDB !== undefined && $window.IDBTransaction !== undefined // IndexedDB is REALLY slow without blob support in Safari 8, no point in it if (storageIsAvailable && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1 && navigator.userAgent.match(/Version\/[678]/) ) { storageIsAvailable = false } var storeBlobsAvailable = storageIsAvailable || false function isAvailable () { return storageIsAvailable } function openDatabase () { if (openDbPromise) { return openDbPromise } try { var request = indexedDB.open(dbName, dbVersion) var deferred = $q.defer() var createObjectStore = function (db) { db.createObjectStore(dbStoreName) } if (!request) { throw new Exception() } } catch (error) { console.error('error opening db', error.message) storageIsAvailable = false return $q.reject(error) } var finished = false setTimeout(function () { if (!finished) { request.onerror({type: 'IDB_CREATE_TIMEOUT'}) } }, 3000) request.onsuccess = function (event) { finished = true db = request.result db.onerror = function (error) { storageIsAvailable = false console.error('Error creating/accessing IndexedDB database', error) deferred.reject(error) } deferred.resolve(db) } request.onerror = function (event) { finished = true storageIsAvailable = false console.error('Error creating/accessing IndexedDB database', event) deferred.reject(event) } request.onupgradeneeded = function (event) { finished = true console.warn('performing idb upgrade from', event.oldVersion, 'to', event.newVersion) var db = event.target.result if (event.oldVersion == 1) { db.deleteObjectStore(dbStoreName) } createObjectStore(db) } return openDbPromise = deferred.promise } function saveFile (fileName, blob) { return openDatabase().then(function (db) { if (!storeBlobsAvailable) { return saveFileBase64(db, fileName, blob) } if (!(blob instanceof Blob)) { blob = blobConstruct([blob]) } try { var objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName) var request = objectStore.put(blob, fileName) } catch (error) { if (storeBlobsAvailable) { storeBlobsAvailable = false return saveFileBase64(db, fileName, blob) } storageIsAvailable = false return $q.reject(error) } var deferred = $q.defer() request.onsuccess = function (event) { deferred.resolve(blob) } request.onerror = function (error) { deferred.reject(error) } return deferred.promise }) } function saveFileBase64 (db, fileName, blob) { if (getBlobSize(blob) > 10 * 1024 * 1024) { return $q.reject() } if (!(blob instanceof Blob)) { var mimeType = blob.type || 'image/jpeg' var address = 'data:' + mimeType + ';base64,' + bytesToBase64(blob) return storagePutB64String(db, fileName, address).then(function () { return blob }) } try { var reader = new FileReader() } catch (e) { storageIsAvailable = false return $q.reject() } var deferred = $q.defer() reader.onloadend = function () { storagePutB64String(db, fileName, reader.result).then(function () { deferred.resolve(blob) }, function (error) { deferred.reject(error) }) } reader.onerror = function (error) { deferred.reject(error) } try { reader.readAsDataURL(blob) } catch (e) { storageIsAvailable = false return $q.reject() } return deferred.promise } function storagePutB64String (db, fileName, b64string) { try { var objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName) var request = objectStore.put(b64string, fileName) } catch (error) { storageIsAvailable = false return $q.reject(error) } var deferred = $q.defer() request.onsuccess = function (event) { deferred.resolve() } request.onerror = function (error) { deferred.reject(error) } return deferred.promise } function getBlobSize (blob) { return blob.size || blob.byteLength || blob.length } function getFile (fileName) { return openDatabase().then(function (db) { var deferred = $q.defer() var objectStore = db.transaction([dbStoreName], IDBTransaction.READ || 'readonly').objectStore(dbStoreName) var request = objectStore.get(fileName) request.onsuccess = function (event) { var result = event.target.result if (result === undefined) { deferred.reject() } else if (typeof result === 'string' && result.substr(0, 5) === 'data:') { deferred.resolve(dataUrlToBlob(result)) } else { deferred.resolve(result) } } request.onerror = function (error) { deferred.reject(error) } return deferred.promise }) } function getFileWriter (fileName, mimeType) { var fakeWriter = FileManager.getFakeFileWriter(mimeType, function (blob) { saveFile(fileName, blob) }) return $q.when(fakeWriter) } openDatabase() return { name: 'IndexedDB', isAvailable: isAvailable, saveFile: saveFile, getFile: getFile, getFileWriter: getFileWriter } }) .service('TmpfsFileStorage', function ($q, $window, FileManager) { $window.requestFileSystem = $window.requestFileSystem || $window.webkitRequestFileSystem var reqFsPromise, fileSystem var storageIsAvailable = $window.requestFileSystem !== undefined function requestFS () { if (reqFsPromise) { return reqFsPromise } if (!$window.requestFileSystem) { return reqFsPromise = $q.reject({type: 'FS_BROWSER_UNSUPPORTED', description: 'requestFileSystem not present'}) } var deferred = $q.defer() $window.requestFileSystem($window.TEMPORARY, 500 * 1024 * 1024, function (fs) { cachedFs = fs deferred.resolve() }, function (e) { storageIsAvailable = false deferred.reject(e) }) return reqFsPromise = deferred.promise } function isAvailable () { return storageIsAvailable } function getFile (fileName, size) { size = size || 1 return requestFS().then(function () { // console.log(dT(), 'get file', fileName) var deferred = $q.defer() cachedFs.root.getFile(fileName, {create: false}, function (fileEntry) { fileEntry.file(function (file) { // console.log(dT(), 'aa', file) if (file.size >= size) { deferred.resolve(fileEntry) } else { deferred.reject(new Error('FILE_NOT_FOUND')) } }, function (error) { console.log(dT(), 'error', error) deferred.reject(error) }) }, function () { deferred.reject(new Error('FILE_NOT_FOUND')) }) return deferred.promise }) } function saveFile (fileName, blob) { return getFileWriter(fileName).then(function (fileWriter) { return FileManager.write(fileWriter, blob).then(function () { return fileWriter.finalize() }) }) } function getFileWriter (fileName) { // console.log(dT(), 'get file writer', fileName) return requestFS().then(function () { var deferred = $q.defer() cachedFs.root.getFile(fileName, {create: true}, function (fileEntry) { FileManager.getFileWriter(fileEntry).then(function (fileWriter) { fileWriter.finalize = function () { return fileEntry } deferred.resolve(fileWriter) }, function (error) { storageIsAvailable = false deferred.reject(error) }) }, function (error) { storageIsAvailable = false deferred.reject(error) }) return deferred.promise }) } requestFS() return { name: 'TmpFS', isAvailable: isAvailable, saveFile: saveFile, getFile: getFile, getFileWriter: getFileWriter } }) .service('MemoryFileStorage', function ($q, FileManager) { var storage = {} function isAvailable () { return true } function getFile (fileName, size) { if (storage[fileName]) { return $q.when(storage[fileName]) } return $q.reject(new Error('FILE_NOT_FOUND')) } function saveFile (fileName, blob) { return $q.when(storage[fileName] = blob) } function getFileWriter (fileName, mimeType) { var fakeWriter = FileManager.getFakeFileWriter(mimeType, function (blob) { saveFile(fileName, blob) }) return $q.when(fakeWriter) } return { name: 'Memory', isAvailable: isAvailable, saveFile: saveFile, getFile: getFile, getFileWriter: getFileWriter } }) .service('WebpManager', function (qSync, $q) { var nativeWebpSupport = false var image = new Image() image.onload = function () { nativeWebpSupport = this.width === 2 && this.height === 1 } image.onerror = function () { nativeWebpSupport = false } image.src = 'data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAACyAgCdASoCAAEALmk0mk0iIiIiIgBoSygABc6zbAAA/v56QAAAAA==' var canvas var context function getCanvasFromWebp (data) { var start = tsNow() var decoder = new WebPDecoder() var config = decoder.WebPDecoderConfig var buffer = config.j || config.output var bitstream = config.input if (!decoder.WebPInitDecoderConfig(config)) { console.error('[webpjs] Library version mismatch!') return false } // console.log('[webpjs] status code', decoder.VP8StatusCode) var StatusCode = decoder.VP8StatusCode status = decoder.WebPGetFeatures(data, data.length, bitstream) if (status != (StatusCode.VP8_STATUS_OK || 0)) { console.error('[webpjs] status error', status, StatusCode) } var mode = decoder.WEBP_CSP_MODE buffer.colorspace = mode.MODE_RGBA buffer.J = 4 try { status = decoder.WebPDecode(data, data.length, config) } catch (e) { status = e } ok = (status == 0) if (!ok) { console.error('[webpjs] decoding failed', status, StatusCode) return false } // console.log('[webpjs] decoded: ', buffer.width, buffer.height, bitstream.has_alpha, 'Now saving...') var bitmap = buffer.c.RGBA.ma // console.log('[webpjs] done in ', tsNow() - start) if (!bitmap) { return false } var biHeight = buffer.height var biWidth = buffer.width if (!canvas || !context) { canvas = document.createElement('canvas') context = canvas.getContext('2d') } else { context.clearRect(0, 0, canvas.width, canvas.height) } canvas.height = biHeight canvas.width = biWidth var output = context.createImageData(canvas.width, canvas.height) var outputData = output.data for (var h = 0; h < biHeight; h++) { for (var w = 0; w < biWidth; w++) { outputData[0 + w * 4 + (biWidth * 4) * h] = bitmap[1 + w * 4 + (biWidth * 4) * h] outputData[1 + w * 4 + (biWidth * 4) * h] = bitmap[2 + w * 4 + (biWidth * 4) * h] outputData[2 + w * 4 + (biWidth * 4) * h] = bitmap[3 + w * 4 + (biWidth * 4) * h] outputData[3 + w * 4 + (biWidth * 4) * h] = bitmap[0 + w * 4 + (biWidth * 4) * h] } } context.putImageData(output, 0, 0) return true } function getPngBlobFromWebp (data) { if (!getCanvasFromWebp(data)) { return $q.reject({type: 'WEBP_PROCESS_FAILED'}) } if (canvas.toBlob === undefined) { return qSync.when(dataUrlToBlob(canvas.toDataURL('image/png'))) } var deferred = $q.defer() canvas.toBlob(function (blob) { deferred.resolve(blob) }, 'image/png') return deferred.promise } return { isWebpSupported: function () { return nativeWebpSupport }, getPngBlobFromWebp: getPngBlobFromWebp } }) .service('CryptoWorker', function ($timeout, $q) { var webWorker = false var naClEmbed = false var taskID = 0 var awaiting = {} var webCrypto = Config.Modes.webcrypto && window.crypto && (window.crypto.subtle || window.crypto.webkitSubtle) /* || window.msCrypto && window.msCrypto.subtle*/ var useSha1Crypto = webCrypto && webCrypto.digest !== undefined var useSha256Crypto = webCrypto && webCrypto.digest !== undefined var finalizeTask = function (taskID, result) { var deferred = awaiting[taskID] if (deferred !== undefined) { // console.log(dT(), 'CW done') deferred.resolve(result) delete awaiting[taskID] } } if (Config.Modes.nacl && navigator.mimeTypes && navigator.mimeTypes['application/x-pnacl'] !== undefined) { var listener = $('
').appendTo($('body'))[0] listener.addEventListener('load', function (e) { naClEmbed = listener.firstChild console.log(dT(), 'NaCl ready') }, true) listener.addEventListener('message', function (e) { finalizeTask(e.data.taskID, e.data.result) }, true) listener.addEventListener('error', function (e) { console.error('NaCl error', e) }, true) } if (window.Worker) { var tmpWorker = new Worker('js/lib/crypto_worker.js') tmpWorker.onmessage = function (e) { if (!webWorker) { webWorker = tmpWorker } else { finalizeTask(e.data.taskID, e.data.result) } } tmpWorker.onerror = function (error) { console.error('CW error', error, error.stack) webWorker = false } } function performTaskWorker (task, params, embed) { // console.log(dT(), 'CW start', task) var deferred = $q.defer() awaiting[taskID] = deferred params.task = task params.taskID = taskID ;(embed || webWorker).postMessage(params) taskID++ return deferred.promise } return { sha1Hash: function (bytes) { if (useSha1Crypto) { // We don't use buffer since typedArray.subarray(...).buffer gives the whole buffer and not sliced one. webCrypto.digest supports typed array var deferred = $q.defer() var bytesTyped = Array.isArray(bytes) ? convertToUint8Array(bytes) : bytes // console.log(dT(), 'Native sha1 start') webCrypto.digest({name: 'SHA-1'}, bytesTyped).then(function (digest) { // console.log(dT(), 'Native sha1 done') deferred.resolve(digest) }, function (e) { console.error('Crypto digest error', e) useSha1Crypto = false deferred.resolve(sha1HashSync(bytes)) }) return deferred.promise } return $timeout(function () { return sha1HashSync(bytes) }) }, sha256Hash: function (bytes) { if (useSha256Crypto) { var deferred = $q.defer() var bytesTyped = Array.isArray(bytes) ? convertToUint8Array(bytes) : bytes // console.log(dT(), 'Native sha1 start') webCrypto.digest({name: 'SHA-256'}, bytesTyped).then(function (digest) { // console.log(dT(), 'Native sha1 done') deferred.resolve(digest) }, function (e) { console.error('Crypto digest error', e) useSha256Crypto = false deferred.resolve(sha256HashSync(bytes)) }) return deferred.promise } return $timeout(function () { return sha256HashSync(bytes) }) }, aesEncrypt: function (bytes, keyBytes, ivBytes) { if (naClEmbed) { return performTaskWorker('aes-encrypt', { bytes: addPadding(convertToArrayBuffer(bytes)), keyBytes: convertToArrayBuffer(keyBytes), ivBytes: convertToArrayBuffer(ivBytes) }, naClEmbed) } return $timeout(function () { return convertToArrayBuffer(aesEncryptSync(bytes, keyBytes, ivBytes)) }) }, aesDecrypt: function (encryptedBytes, keyBytes, ivBytes) { if (naClEmbed) { return performTaskWorker('aes-decrypt', { encryptedBytes: addPadding(convertToArrayBuffer(encryptedBytes)), keyBytes: convertToArrayBuffer(keyBytes), ivBytes: convertToArrayBuffer(ivBytes) }, naClEmbed) } return $timeout(function () { return convertToArrayBuffer(aesDecryptSync(encryptedBytes, keyBytes, ivBytes)) }) }, factorize: function (bytes) { bytes = convertToByteArray(bytes) if (naClEmbed && bytes.length <= 8) { return performTaskWorker('factorize', {bytes: bytes}, naClEmbed) } if (webWorker) { return performTaskWorker('factorize', {bytes: bytes}) } return $timeout(function () { return pqPrimeFactorization(bytes) }) }, modPow: function (x, y, m) { if (webWorker) { return performTaskWorker('mod-pow', { x: x, y: y, m: m }) } return $timeout(function () { return bytesModPow(x, y, m) }) } } }) .service('ExternalResourcesManager', function ($q, $http, $sce) { var urlPromises = {} function downloadByURL (url) { if (urlPromises[url] !== undefined) { return urlPromises[url] } return urlPromises[url] = $http.get(url, {responseType: 'blob', transformRequest: null}) .then(function (response) { window.URL = window.URL || window.webkitURL var url = window.URL.createObjectURL(response.data) return $sce.trustAsResourceUrl(url) }, function (error) { if (!Config.Modes.chrome_packed) { return $q.when($sce.trustAsResourceUrl(url)) } return $q.reject(error) }) } return { downloadByURL: downloadByURL } }) .service('IdleManager', function ($rootScope, $window, $timeout) { $rootScope.idle = {isIDLE: false} var toPromise var started = false var hidden = 'hidden' var visibilityChange = 'visibilitychange' if (typeof document.hidden !== 'undefined') { // default } else if (typeof document.mozHidden !== 'undefined') { hidden = 'mozHidden' visibilityChange = 'mozvisibilitychange' } else if (typeof document.msHidden !== 'undefined') { hidden = 'msHidden' visibilityChange = 'msvisibilitychange' } else if (typeof document.webkitHidden !== 'undefined') { hidden = 'webkitHidden' visibilityChange = 'webkitvisibilitychange' } return { start: start } function start () { if (!started) { started = true $($window).on(visibilityChange + ' blur focus keydown mousedown touchstart', onEvent) setTimeout(function () { onEvent({type: 'blur'}) }, 0) } } function onEvent (e) { // console.log('event', e.type) if (e.type == 'mousemove') { var e = e.originalEvent || e if (e && e.movementX === 0 && e.movementY === 0) { return } $($window).off('mousemove', onEvent) } var isIDLE = e.type == 'blur' || e.type == 'timeout' ? true : false if (hidden && document[hidden]) { isIDLE = true } $timeout.cancel(toPromise) if (!isIDLE) { // console.log('update timeout') toPromise = $timeout(function () { onEvent({type: 'timeout'}) }, 30000) } if (e.type == 'focus' && !$rootScope.idle.afterFocus) { $rootScope.idle.afterFocus = true setTimeout(function () { delete $rootScope.idle.afterFocus }, 10) } if ($rootScope.idle.isIDLE == isIDLE) { return } // console.log('IDLE changed', isIDLE) $rootScope.$apply(function () { $rootScope.idle.isIDLE = isIDLE }) if (isIDLE && e.type == 'timeout') { $($window).on('mousemove', onEvent) } } }) .service('GeoLocationManager', function ($q) { var lastCoords = false function isAvailable () { return navigator.geolocation !== undefined } function getPosition (force) { if (!force && lastCoords) { return $q.when(lastCoords) } if (!isAvailable()) { return $q.reject() } var deferred = $q.defer() navigator.geolocation.getCurrentPosition(function (position) { lastCoords = { lat: position.coords.latitude, long: position.coords.longitude } deferred.resolve(lastCoords) }, function (error) { deferred.reject(error) }) return deferred.promise } return { getPosition: getPosition, isAvailable: isAvailable } }) .service('AppRuntimeManager', function ($window) { return { reload: function () { try { location.reload() } catch (e) {} if ($window.chrome && chrome.runtime && chrome.runtime.reload) { chrome.runtime.reload() } }, close: function () { try { $window.close() } catch (e) {} }, focus: function () { if (window.navigator.mozApps && document.hidden) { // Get app instance and launch it to bring app to foreground window.navigator.mozApps.getSelf().onsuccess = function () { this.result.launch() } } else { if (window.chrome && chrome.app && chrome.app.window) { chrome.app.window.current().focus() } window.focus() } } } }) .service('RichTextProcessor', function ($sce, $sanitize) { var emojiMap = {} var emojiData = Config.Emoji var emojiIconSize = 18 var emojiSupported = navigator.userAgent.search(/OS X|iPhone|iPad|iOS|Android/i) != -1, emojiCode var emojiRegExp = '\\u0023\\u20E3|\\u00a9|\\u00ae|\\u203c|\\u2049|\\u2139|[\\u2194-\\u2199]|\\u21a9|\\u21aa|\\u231a|\\u231b|\\u23e9|[\\u23ea-\\u23ec]|\\u23f0|\\u24c2|\\u25aa|\\u25ab|\\u25b6|\\u2611|\\u2614|\\u26fd|\\u2705|\\u2709|[\\u2795-\\u2797]|\\u27a1|\\u27b0|\\u27bf|\\u2934|\\u2935|[\\u2b05-\\u2b07]|\\u2b1b|\\u2b1c|\\u2b50|\\u2b55|\\u3030|\\u303d|\\u3297|\\u3299|[\\uE000-\\uF8FF\\u270A-\\u2764\\u2122\\u25C0\\u25FB-\\u25FE\\u2615\\u263a\\u2648-\\u2653\\u2660-\\u2668\\u267B\\u267F\\u2693\\u261d\\u26A0-\\u26FA\\u2708\\u2702\\u2601\\u260E]|[\\u2600\\u26C4\\u26BE\\u23F3\\u2764]|\\uD83D[\\uDC00-\\uDFFF]|\\uD83C[\\uDDE8-\\uDDFA\uDDEC]\\uD83C[\\uDDEA-\\uDDFA\uDDE7]|[0-9]\\u20e3|\\uD83C[\\uDC00-\\uDFFF]' for (emojiCode in emojiData) { emojiMap[emojiData[emojiCode][0]] = emojiCode } var alphaCharsRegExp = 'a-z' + '\\u00c0-\\u00d6\\u00d8-\\u00f6\\u00f8-\\u00ff' + // Latin-1 '\\u0100-\\u024f' + // Latin Extended A and B '\\u0253\\u0254\\u0256\\u0257\\u0259\\u025b\\u0263\\u0268\\u026f\\u0272\\u0289\\u028b' + // IPA Extensions '\\u02bb' + // Hawaiian '\\u0300-\\u036f' + // Combining diacritics '\\u1e00-\\u1eff' + // Latin Extended Additional (mostly for Vietnamese) '\\u0400-\\u04ff\\u0500-\\u0527' + // Cyrillic '\\u2de0-\\u2dff\\ua640-\\ua69f' + // Cyrillic Extended A/B '\\u0591-\\u05bf\\u05c1-\\u05c2\\u05c4-\\u05c5\\u05c7' + '\\u05d0-\\u05ea\\u05f0-\\u05f4' + // Hebrew '\\ufb1d-\\ufb28\\ufb2a-\\ufb36\\ufb38-\\ufb3c\\ufb3e\\ufb40-\\ufb41' + '\\ufb43-\\ufb44\\ufb46-\\ufb4f' + // Hebrew Pres. Forms '\\u0610-\\u061a\\u0620-\\u065f\\u066e-\\u06d3\\u06d5-\\u06dc' + '\\u06de-\\u06e8\\u06ea-\\u06ef\\u06fa-\\u06fc\\u06ff' + // Arabic '\\u0750-\\u077f\\u08a0\\u08a2-\\u08ac\\u08e4-\\u08fe' + // Arabic Supplement and Extended A '\\ufb50-\\ufbb1\\ufbd3-\\ufd3d\\ufd50-\\ufd8f\\ufd92-\\ufdc7\\ufdf0-\\ufdfb' + // Pres. Forms A '\\ufe70-\\ufe74\\ufe76-\\ufefc' + // Pres. Forms B '\\u200c' + // Zero-Width Non-Joiner '\\u0e01-\\u0e3a\\u0e40-\\u0e4e' + // Thai '\\u1100-\\u11ff\\u3130-\\u3185\\uA960-\\uA97F\\uAC00-\\uD7AF\\uD7B0-\\uD7FF' + // Hangul (Korean) '\\u3003\\u3005\\u303b' + // Kanji/Han iteration marks '\\uff21-\\uff3a\\uff41-\\uff5a' + // full width Alphabet '\\uff66-\\uff9f' + // half width Katakana '\\uffa1-\\uffdc'; // half width Hangul (Korean) var alphaNumericRegExp = '0-9\_' + alphaCharsRegExp var domainAddChars = '\u00b7' // Based on Regular Expression for URL validation by Diego Perini var urlRegExp = '((?:https?|ftp)://|mailto:)?' + // user:pass authentication '(?:\\S{1,64}(?::\\S{0,64})?@)?' + '(?:' + // sindresorhus/ip-regexp '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}' + '|' + // host name '[' + alphaCharsRegExp + '0-9][' + alphaCharsRegExp + domainAddChars + '0-9\-]{0,64}' + // domain name '(?:\\.[' + alphaCharsRegExp + '0-9][' + alphaCharsRegExp + domainAddChars + '0-9\-]{0,64}){0,10}' + // TLD identifier '(?:\\.(xn--[0-9a-z]{2,16}|[' + alphaCharsRegExp + ']{2,24}))' + ')' + // port number '(?::\\d{2,5})?' + // resource path '(?:/(?:\\S{0,255}[^\\s.;,(\\[\\]{}<>"\'])?)?' var usernameRegExp = '[a-zA-Z\\d_]{5,32}' var botCommandRegExp = '\\/([a-zA-Z\\d_]{1,32})(?:@(' + usernameRegExp + '))?(\\b|$)' var fullRegExp = new RegExp('(^| )(@)(' + usernameRegExp + ')|(' + urlRegExp + ')|(\\n)|(' + emojiRegExp + ')|(^|[\\s\\(\\]])(#[' + alphaNumericRegExp + ']{2,64})|(^|\\s)' + botCommandRegExp, 'i') var emailRegExp = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ var youtubeRegExp = /^(?:https?:\/\/)?(?:www\.)?youtu(?:|\.be|be\.com|\.b)(?:\/v\/|\/watch\\?v=|e\/|(?:\/\??#)?\/watch(?:.+)v=)(.{11})(?:\&[^\s]*)?/ var vimeoRegExp = /^(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/ var instagramRegExp = /^https?:\/\/(?:instagr\.am\/p\/|instagram\.com\/p\/)([a-zA-Z0-9\-\_]+)/i var vineRegExp = /^https?:\/\/vine\.co\/v\/([a-zA-Z0-9\-\_]+)/i var twitterRegExp = /^https?:\/\/twitter\.com\/.+?\/status\/\d+/i var facebookRegExp = /^https?:\/\/(?:www\.|m\.)?facebook\.com\/(?:.+?\/posts\/\d+|(?:story\.php|permalink\.php)\?story_fbid=(\d+)(?:&substory_index=\d+)?&id=(\d+))/i var gplusRegExp = /^https?:\/\/plus\.google\.com\/\d+\/posts\/[a-zA-Z0-9\-\_]+/i var soundcloudRegExp = /^https?:\/\/(?:soundcloud\.com|snd\.sc)\/([a-zA-Z0-9%\-\_]+)\/([a-zA-Z0-9%\-\_]+)/i var spotifyRegExp = /(https?:\/\/(open\.spotify\.com|play\.spotify\.com|spoti\.fi)\/(.+)|spotify:(.+))/i var markdownRegExp = /(^|\s)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s)`([^\n]+?)`([\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)/ var siteHashtags = { Telegram: 'tg://search_hashtag?hashtag={1}', Twitter: 'https://twitter.com/hashtag/{1}', Instagram: 'https://instagram.com/explore/tags/{1}/', 'Google Plus': 'https://plus.google.com/explore/{1}' } var siteMentions = { Telegram: '#/im?p=%40{1}', Twitter: 'https://twitter.com/{1}', Instagram: 'https://instagram.com/{1}/', GitHub: 'https://github.com/{1}' } return { wrapRichText: wrapRichText, wrapPlainText: wrapPlainText, wrapDraftText: wrapDraftText, wrapUrl: wrapUrl, parseEntities: parseEntities, parseMarkdown: parseMarkdown, parseEmojis: parseEmojis, mergeEntities: mergeEntities } function getEmojiSpritesheetCoords (emojiCode) { var i var row, column var totalColumns for (var cat = 0; cat < Config.EmojiCategories.length; cat++) { totalColumns = Config.EmojiCategorySpritesheetDimens[cat][1] i = Config.EmojiCategories[cat].indexOf(emojiCode) if (i > -1) { row = Math.floor(i / totalColumns) column = (i % totalColumns) return { category: cat, row: row, column: column } } } console.error('emoji not found in spritesheet', emojiCode) return null } function parseEntities (text, options) { options = options || {} var match var raw = text, url var entities = [], emojiCode, emojiCoords, matchIndex var rawOffset = 0 // var start = tsNow() while ((match = raw.match(fullRegExp))) { matchIndex = rawOffset + match.index if (match[3]) { // mentions entities.push({ _: 'messageEntityMention', offset: matchIndex + match[1].length, length: match[2].length + match[3].length }) } else if (match[4]) { if (emailRegExp.test(match[4])) { // email entities.push({ _: 'messageEntityEmail', offset: matchIndex, length: match[4].length }) } else { var url = false var protocol = match[5] var tld = match[6] var excluded = '' if (tld) { // URL if (!protocol && (tld.substr(0, 4) === 'xn--' || Config.TLD.indexOf(tld.toLowerCase()) !== -1)) { protocol = 'http://' } if (protocol) { var balanced = checkBrackets(match[4]) if (balanced.length !== match[4].length) { excluded = match[4].substring(balanced.length) match[4] = balanced } url = (match[5] ? '' : protocol) + match[4] } } else { // IP address url = (match[5] ? '' : 'http://') + match[4] } if (url) { entities.push({ _: 'messageEntityUrl', offset: matchIndex, length: match[4].length }) } } } else if (match[7]) { // New line entities.push({ _: 'messageEntityLinebreak', offset: matchIndex, length: 1 }) } else if (match[8]) { // Emoji if ((emojiCode = emojiMap[match[8]]) && (emojiCoords = getEmojiSpritesheetCoords(emojiCode))) { entities.push({ _: 'messageEntityEmoji', offset: matchIndex, length: match[0].length, coords: emojiCoords, title: emojiData[emojiCode][1][0] }) } } else if (match[10]) { // Hashtag entities.push({ _: 'messageEntityHashtag', offset: matchIndex + match[9].length, length: match[10].length }) } else if (match[12]) { // Bot command entities.push({ _: 'messageEntityBotCommand', offset: matchIndex + match[11].length, length: 1 + match[12].length + (match[13] ? 1 + match[13].length : 0) }) } raw = raw.substr(match.index + match[0].length) rawOffset += match.index + match[0].length } // if (entities.length) { // console.log('parse entities', text, entities.slice()) // } return entities } function parseEmojis (text) { return text.replace(/:([a-z0-9\-\+\*_]+?):/gi, function (all, shortcut) { var emojiCode = EmojiHelper.shortcuts[shortcut] if (emojiCode !== undefined) { return EmojiHelper.emojis[emojiCode][0] } return all }) } function parseMarkdown (text, entities) { if (text.indexOf('`') == -1 && text.indexOf('@') == -1) { return text.trim() } var raw = text var match var newText = [] var rawOffset = 0 var matchIndex while (match = raw.match(markdownRegExp)) { matchIndex = rawOffset + match.index newText.push(raw.substr(0, match.index)) var text = (match[3] || match[7] || match[10]) rawOffset -= text.length text = text.replace(/^\s+|\s+$/g, '') rawOffset += text.length if (text.match(/^`*$/)) { newText.push(match[0]) } else if (match[3]) { // pre if (match[5] == '\n') { match[5] = '' rawOffset -= 1 } newText.push(match[1] + text + match[5]) entities.push({ _: 'messageEntityPre', language: '', offset: matchIndex + match[1].length, length: text.length }) rawOffset -= match[2].length + match[4].length } else if (match[7]) { // code newText.push(match[6] + text + match[8]) entities.push({ _: 'messageEntityCode', offset: matchIndex + match[6].length, length: text.length }) rawOffset -= 2 } else if (match[10]) { // custom mention newText.push(text) entities.push({ _: 'messageEntityMentionName', user_id: match[9], offset: matchIndex, length: text.length }) rawOffset -= match[0] - text.length } raw = raw.substr(match.index + match[0].length) rawOffset += match.index + match[0].length } newText.push(raw) newText = newText.join('') if (!newText.replace(/\s+/g, '').length) { newText = text entities.splice(0, entities.length) } if (!entities.length) { newText = newText.trim() } return newText } function mergeEntities (currentEntities, newEntities, fromApi) { var totalEntities = newEntities.slice() var i var len = currentEntities.length var j var len2 = newEntities.length var startJ = 0 var curEntity var newEntity var start, end var cStart, cEnd var bad for (i = 0; i < len; i++) { curEntity = currentEntities[i] if (fromApi && curEntity._ != 'messageEntityLinebreak' && curEntity._ != 'messageEntityEmoji') { continue } // console.log('s', curEntity, newEntities) start = curEntity.offset end = start + curEntity.length bad = false for (j = startJ; j < len2; j++) { newEntity = newEntities[j] cStart = newEntity.offset cEnd = cStart + newEntity.length if (cStart <= start) { startJ = j } if (start >= cStart && start < cEnd || end > cStart && end <= cEnd) { // console.log('bad', curEntity, newEntity) if (fromApi && start >= cStart && end <= cEnd) { if (newEntity.nested === undefined) { newEntity.nested = [] } curEntity.offset -= cStart newEntity.nested.push(angular.copy(curEntity)) } bad = true break } if (cStart >= end) { break } } if (bad) { continue } totalEntities.push(curEntity) } totalEntities.sort(function (a, b) { return a.offset - b.offset }) // console.log('merge', currentEntities, newEntities, totalEntities) return totalEntities } function wrapRichNestedText (text, nested, options) { if (nested === undefined) { return encodeEntities(text) } options.hasNested = true return wrapRichText(text, {entities: nested, nested: true}) } function wrapRichText (text, options) { if (!text || !text.length) { return '' } options = options || {} var entities = options.entities var contextSite = options.contextSite || 'Telegram' var contextExternal = contextSite != 'Telegram' var emojiFound = false if (entities === undefined) { entities = parseEntities(text, options) } var i = 0 var len = entities.length var entity var entityText var skipEntity var url var html = [] var lastOffset = 0 var curEmojiSize = options.emojiIconSize || emojiIconSize for (i = 0; i < len; i++) { entity = entities[i] if (entity.offset > lastOffset) { html.push( encodeEntities(text.substr(lastOffset, entity.offset - lastOffset)) ) } else if (entity.offset < lastOffset) { continue } skipEntity = false entityText = text.substr(entity.offset, entity.length) switch (entity._) { case 'messageEntityMention': var contextUrl = !options.noLinks && siteMentions[contextSite] if (!contextUrl) { skipEntity = true break } var username = entityText.substr(1) var attr = '' if (options.highlightUsername && options.highlightUsername.toLowerCase() == username.toLowerCase()) { attr = 'class="im_message_mymention"' } html.push( '', encodeEntities(entityText), '' ) break case 'messageEntityMentionName': if (options.noLinks) { skipEntity = true break } html.push( '', encodeEntities(entityText), '' ) break case 'messageEntityHashtag': var contextUrl = !options.noLinks && siteHashtags[contextSite] if (!contextUrl) { skipEntity = true break } var hashtag = entityText.substr(1) html.push( '', encodeEntities(entityText), '' ) break case 'messageEntityEmail': if (options.noLinks) { skipEntity = true break } html.push( '', encodeEntities(entityText), '' ) break case 'messageEntityUrl': case 'messageEntityTextUrl': if (options.noLinks) { skipEntity = true break } var url = entity.url || entityText url = wrapUrl(url, entity._ == 'messageEntityTextUrl' ? 2 : false) html.push( '', wrapRichNestedText(entityText, entity.nested, options), '' ) break case 'messageEntityLinebreak': html.push(options.noLinebreaks ? ' ' : '
') break case 'messageEntityEmoji': html.push( '', ':', entity.title, ':' ) emojiFound = true break case 'messageEntityBotCommand': if (options.noLinks || options.noCommands || contextExternal) { skipEntity = true break } var command = entityText.substr(1) var bot var atPos if ((atPos = command.indexOf('@')) != -1) { bot = command.substr(atPos + 1) command = command.substr(0, atPos) } else { bot = options.fromBot } html.push( '', encodeEntities(entityText), '' ) break case 'messageEntityBold': html.push( '', wrapRichNestedText(entityText, entity.nested, options), '' ) break case 'messageEntityItalic': html.push( '', wrapRichNestedText(entityText, entity.nested, options), '' ) break case 'messageEntityCode': html.push( '', encodeEntities(entityText), '' ) break case 'messageEntityPre': html.push( '
',
              encodeEntities(entityText),
              '
' ) break default: skipEntity = true } lastOffset = entity.offset + (skipEntity ? 0 : entity.length) } html.push(encodeEntities(text.substr(lastOffset))) text = $sanitize(html.join('')) if (!options.nested && (emojiFound || options.hasNested)) { text = text.replace(/\ufe0f|️|�|‍/g, '', text) var emojiSizeClass = curEmojiSize == 18 ? '' : (' emoji-w' + curEmojiSize) text = text.replace(//g, '') } return $sce.trustAs('html', text) } function wrapDraftText (text, options) { if (!text || !text.length) { return '' } options = options || {} var entities = options.entities if (entities === undefined) { entities = parseEntities(text, options) } var i = 0 var len = entities.length var entity var entityText var skipEntity var code = [] var lastOffset = 0 for (i = 0; i < len; i++) { entity = entities[i] if (entity.offset > lastOffset) { code.push( text.substr(lastOffset, entity.offset - lastOffset) ) } else if (entity.offset < lastOffset) { continue } skipEntity = false entityText = text.substr(entity.offset, entity.length) switch (entity._) { case 'messageEntityEmoji': code.push( ':', entity.title, ':' ) break case 'messageEntityCode': code.push( '`', entityText, '`' ) break case 'messageEntityPre': code.push( '```', entityText, '```' ) break case 'messageEntityMentionName': code.push( '@', entity.user_id, ' (', entityText, ')' ) break default: skipEntity = true } lastOffset = entity.offset + (skipEntity ? 0 : entity.length) } code.push(text.substr(lastOffset)) return code.join('') } function checkBrackets (url) { var urlLength = url.length var urlOpenBrackets = url.split('(').length - 1 var urlCloseBrackets = url.split(')').length - 1 while (urlCloseBrackets > urlOpenBrackets && url.charAt(urlLength - 1) === ')') { url = url.substr(0, urlLength - 1) urlCloseBrackets-- urlLength-- } if (urlOpenBrackets > urlCloseBrackets) { url = url.replace(/\)+$/, '') } return url } function wrapPlainText (text, options) { if (emojiSupported) { return text } if (!text || !text.length) { return '' } options = options || {} text = text.replace(/\ufe0f/g, '', text) var match var raw = text var text = [], emojiTitle while ((match = raw.match(fullRegExp))) { text.push(raw.substr(0, match.index)) if (match[8]) { if ((emojiCode = emojiMap[match[8]]) && (emojiTitle = emojiData[emojiCode][1][0])) { text.push(':' + emojiTitle + ':') } else { text.push(match[0]) } } else { text.push(match[0]) } raw = raw.substr(match.index + match[0].length) } text.push(raw) return text.join('') } function wrapUrl (url, unsafe) { if (!url.match(/^https?:\/\//i)) { url = 'http://' + url } var tgMeMatch if (unsafe == 2) { url = 'tg://unsafe_url?url=' + encodeURIComponent(url) } else if ( (tgMeMatch = url.match(/^https?:\/\/telegram\.me\/(.+)/))) { var path = tgMeMatch[1].split('/') switch (path[0]) { case 'joinchat': url = 'tg://join?invite=' + path[1] break case 'addstickers': url = 'tg://addstickers?set=' + path[1] break default: if (path[1] && path[1].match(/^\d+$/)) { url = 'tg://resolve?domain=' + path[0] + '&post=' + path[1] } else if (!path[1]) { var domainQuery = path[0].split('?') url = 'tg://resolve?domain=' + domainQuery[0] + (domainQuery[1] ? '&' + domainQuery[1] : '') } } } else if (unsafe) { url = 'tg://unsafe_url?url=' + encodeURIComponent(url) } return url } }) .service('ServerTimeManager', function (Storage) { var timestampNow = tsNow(true) var midnightNoOffset = timestampNow - (timestampNow % 86400) var midnightOffseted = new Date() midnightOffseted.setHours(0) midnightOffseted.setMinutes(0) midnightOffseted.setSeconds(0) var midnightOffset = midnightNoOffset - (Math.floor(+midnightOffseted / 1000)) var serverTimeOffset = 0 var timeParams = { midnightOffset: midnightOffset, serverTimeOffset: serverTimeOffset } Storage.get('server_time_offset').then(function (to) { if (to) { serverTimeOffset = to timeParams.serverTimeOffset = to } }) return timeParams })