diff --git a/bower.json b/bower.json index 36ff7e909c..8d9d66245d 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "uProxy", - "version": "0.8.13", + "version": "0.8.14", "dependencies": { "polymer": "^0.5.6", "paper-elements": "Polymer/paper-elements#^0.5.6", diff --git a/package.json b/package.json index 7039d527b3..a7c79edaba 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "uProxy", "description": "Share your pathway to the Internet", - "version": "0.8.13", + "version": "0.8.14", "repository": { "type": "git", "url": "https://github.com/uproxy/uproxy" @@ -47,7 +47,7 @@ "lodash": "^3.7.0", "regex2dfa": "^0.1.6", "tsd": "^0.5.7", - "uproxy-lib": "^27.2.5", + "uproxy-lib": "^28.0.0", "utransformers": "^0.2.1", "xregexp": "^2.0.0" }, diff --git a/src/chrome/app/manifest.json b/src/chrome/app/manifest.json index 8dae33101f..a240694985 100644 --- a/src/chrome/app/manifest.json +++ b/src/chrome/app/manifest.json @@ -3,7 +3,7 @@ "name": "__MSG_appName__", "description": "__MSG_appDescription__", "minimum_chrome_version": "41.0.2272.63", - "version": "0.8.13", + "version": "0.8.14", "default_locale": "en", "icons": { "128": "icons/128_online.png" diff --git a/src/chrome/extension/manifest.json b/src/chrome/extension/manifest.json index ca774d206d..297dddfaf7 100644 --- a/src/chrome/extension/manifest.json +++ b/src/chrome/extension/manifest.json @@ -1,6 +1,6 @@ { "name": "__MSG_extName__", - "version": "0.8.13", + "version": "0.8.14", "manifest_version": 2, "description": "__MSG_extDescription__", "minimum_chrome_version": "41.0.2272.63", diff --git a/src/chrome/extension/scripts/background.ts b/src/chrome/extension/scripts/background.ts index af7d49a5f8..40a82980e8 100644 --- a/src/chrome/extension/scripts/background.ts +++ b/src/chrome/extension/scripts/background.ts @@ -15,12 +15,11 @@ import UiApi = require('../../../interfaces/ui'); import user_interface = require('../../../generic_ui/scripts/ui'); import CoreConnector = require('../../../generic_ui/scripts/core_connector'); import uproxy_core_api = require('../../../interfaces/uproxy_core_api'); +import Constants = require('../../../generic_ui/scripts/constants'); /// /// -export import model = user_interface.model; - // --------------------- Communicating with the App ---------------------------- export var browserConnector :ChromeCoreConnector; // way for ui to speak to a uProxy.CoreApi export var core :CoreConnector; // way for ui to speak to a uProxy.CoreApi @@ -63,6 +62,10 @@ chrome.runtime.onMessageExternal.addListener((request :any, sender :chrome.runti * updates from the Chrome App side propogate to the UI. */ browserApi = new ChromeBrowserApi(); +browserConnector = new ChromeCoreConnector({ name: 'uproxy-extension-to-app-port' }); +browserConnector.onUpdate(uproxy_core_api.Update.LAUNCH_UPROXY, + browserApi.bringUproxyToFront); + // TODO (lucyhe): Make sure that the "install" event isn't missed if we // are adding the listener after the event is fired. chrome.runtime.onInstalled.addListener((details :chrome.runtime.InstalledDetails) => { @@ -70,6 +73,14 @@ chrome.runtime.onInstalled.addListener((details :chrome.runtime.InstalledDetails // we only want to launch the window on the first install return; } + browserConnector.onceConnected.then(() => { + chrome.browserAction.setIcon({ + path: { + "19" : "icons/19_" + Constants.DEFAULT_ICON, + "38" : "icons/38_" + Constants.DEFAULT_ICON, + } + }); + }); chrome.tabs.query({currentWindow: true, active: true}, function(tabs){ // Do not open the extension when it's installed if the user is @@ -85,10 +96,6 @@ chrome.browserAction.onClicked.addListener((tab) => { browserApi.bringUproxyToFront(); }); -browserConnector = new ChromeCoreConnector({ name: 'uproxy-extension-to-app-port' }); -browserConnector.onUpdate(uproxy_core_api.Update.LAUNCH_UPROXY, - browserApi.bringUproxyToFront); - core = new CoreConnector(browserConnector); var oAuth = new ChromeTabAuth(); browserConnector.onUpdate(uproxy_core_api.Update.GET_CREDENTIALS, diff --git a/src/chrome/extension/scripts/chrome_browser_api.ts b/src/chrome/extension/scripts/chrome_browser_api.ts index 00e3c9ffcf..df8c1e12a0 100644 --- a/src/chrome/extension/scripts/chrome_browser_api.ts +++ b/src/chrome/extension/scripts/chrome_browser_api.ts @@ -57,7 +57,7 @@ class ChromeBrowserApi implements BrowserAPI { private popupState_ = PopupState.NOT_LAUNCHED; - public fulfillLaunched : () => void; + public handlePopupLaunch :() => void; private onceLaunched_ :Promise; constructor() { @@ -169,7 +169,7 @@ class ChromeBrowserApi implements BrowserAPI { // after webstore installation), then allow the popup to open at a default // location. this.onceLaunched_ = new Promise((F, R) => { - this.fulfillLaunched = F; + this.handlePopupLaunch = F; }); chrome.windows.create({url: this.POPUP_URL, type: "popup", diff --git a/src/chrome/extension/scripts/chrome_core_connector.spec.ts b/src/chrome/extension/scripts/chrome_core_connector.spec.ts index 9a61b6abef..b0f1984dbd 100644 --- a/src/chrome/extension/scripts/chrome_core_connector.spec.ts +++ b/src/chrome/extension/scripts/chrome_core_connector.spec.ts @@ -39,7 +39,8 @@ describe('core-connector', () => { chromeBrowserApi = jasmine.createSpyObj('ChromeBrowserApi', ['bringUproxyToFront', 'showNotification', - 'on']); + 'on', + 'handlePopupLaunch']); diff --git a/src/chrome/extension/scripts/chrome_tab_auth.ts b/src/chrome/extension/scripts/chrome_tab_auth.ts index 2aa63f791f..af49bb72c4 100644 --- a/src/chrome/extension/scripts/chrome_tab_auth.ts +++ b/src/chrome/extension/scripts/chrome_tab_auth.ts @@ -34,7 +34,7 @@ class ChromeTabAuth { } }; - var isActive = !user_interface.model.reconnecting; + var isActive = true; //TODO use actual value chrome.tabs.create({url: url, active: isActive}, function(tab: chrome.tabs.Tab) { if (isActive) { diff --git a/src/chrome/extension/scripts/context.ts b/src/chrome/extension/scripts/context.ts index 020261e3b5..bbe1ffdba6 100644 --- a/src/chrome/extension/scripts/context.ts +++ b/src/chrome/extension/scripts/context.ts @@ -13,7 +13,7 @@ export var browserConnector :browser_connector.CoreBrowserConnector = ui_context export var ui :user_interface.UserInterface = new user_interface.UserInterface(core, ui_context.browserApi); -export var model :user_interface.Model = user_interface.model; +export var model :user_interface.Model = ui.model; ui.browser = 'chrome'; console.log('Loaded dependencies for Chrome Extension.'); diff --git a/src/firefox/data/scripts/background.ts b/src/firefox/data/scripts/background.ts index 0c2ebd18ab..75ef962dd7 100644 --- a/src/firefox/data/scripts/background.ts +++ b/src/firefox/data/scripts/background.ts @@ -3,10 +3,10 @@ import CoreConnector = require('../../../generic_ui/scripts/core_connector'); import FirefoxCoreConnector = require('./firefox_connector'); import FirefoxBrowserApi = require('./firefox_browser_api'); -export import model = user_interface.model; export var ui :user_interface.UserInterface; export var core :CoreConnector; export var browserConnector: FirefoxCoreConnector; +export var model :user_interface.Model; function initUI() { browserConnector = new FirefoxCoreConnector(); core = new CoreConnector(browserConnector); @@ -17,6 +17,7 @@ function initUI() { if (undefined === ui) { ui = initUI(); + model = ui.model; } ui.browser = 'firefox'; diff --git a/src/firefox/data/scripts/firefox_browser_api.ts b/src/firefox/data/scripts/firefox_browser_api.ts index b4463d58be..97124a35e1 100644 --- a/src/firefox/data/scripts/firefox_browser_api.ts +++ b/src/firefox/data/scripts/firefox_browser_api.ts @@ -33,9 +33,8 @@ class FirefoxBrowserApi implements BrowserAPI { port.on('emitRejected', this.handleEmitRejected_); } - // Firefox doesn't ever need to wait for popup to open, - // We don't have onceLaunched promise, so fulfillLaunched is empty. - public fulfillLaunched = () => { + // Firefox has no work to do on initial launch + public handlePopupLaunch = () => { } public setIcon = (iconFile :string) : void => { diff --git a/src/firefox/package.json b/src/firefox/package.json index 83c3bf04b4..81065d9ad1 100644 --- a/src/firefox/package.json +++ b/src/firefox/package.json @@ -5,7 +5,7 @@ "description": "This is the alpha version of uProxy.", "author": "uProxy Team ", "license": "Apache 2.0", - "version": "0.8.13", + "version": "0.8.14", "permissions": { "private-browsing": true } diff --git a/src/generic_core/diagnose-nat.ts b/src/generic_core/diagnose-nat.ts index abc91abc68..7c1b49af8d 100644 --- a/src/generic_core/diagnose-nat.ts +++ b/src/generic_core/diagnose-nat.ts @@ -613,13 +613,6 @@ export function doUdpTest() { } socket.bind('0.0.0.0', 0) - .then((result :number) => { - if (result != 0) { - return Promise.reject(new Error('listen failed to bind :5758' + - ' with result code ' + result)); - } - return Promise.resolve(result); - }) .then(socket.getInfo) .then((socketInfo: freedom_UdpSocket.SocketInfo) => { log.debug('listening on %1:%2', @@ -694,13 +687,7 @@ function pingStunServer(serverAddr: string) { var bytes = Turn.formatStunMessage(bindRequest); socket.bind('0.0.0.0', 0) - .then((result: number) => { - if (result != 0) { - return Promise.reject(new Error('listen failed to bind :5758' + - ' with result code ' + result)); - } - return Promise.resolve(result); - }).then(() => { + .then(() => { return socket.sendTo(bytes.buffer, parts[1], parseInt(parts[2])); }).then((written: number) => { log.debug('%1 bytes sent correctly', [written]); @@ -769,12 +756,6 @@ export function doNatProvoking() :Promise { socket.on('onData', onUdpData); socket.bind('0.0.0.0', 0) - .then((result: number) => { - if (result != 0) { - return Promise.reject(new Error('failed to bind to a port: err=' + result)); - } - return Promise.resolve(result); - }) .then(socket.getInfo) .then((socketInfo: freedom_UdpSocket.SocketInfo) => { log.debug('listening on %1:%2', @@ -879,28 +860,24 @@ export function probePmpSupport(routerIp:string, privateIp:string) :Promise { - if (result != 0) { - R(new Error('Failed to bind to a port: Err= ' + result)); - } - - // Construct the NAT-PMP map request as an ArrayBuffer - // Map internal port 55555 to external port 55555 w/ 120 sec lifetime - var pmpBuffer = new ArrayBuffer(12); - var pmpView = new DataView(pmpBuffer); - // Version and OP fields (1 byte each) - pmpView.setInt8(0, 0); - pmpView.setInt8(1, 1); - // Reserved, internal port, external port fields (2 bytes each) - pmpView.setInt16(2, 0, false); - pmpView.setInt16(4, 55555, false); - pmpView.setInt16(6, 55555, false); - // Mapping lifetime field (4 bytes) - pmpView.setInt32(8, 120, false); - - socket.sendTo(pmpBuffer, routerIp, 5351); - }); + socket.bind('0.0.0.0', 0). + then(() => { + // Construct the NAT-PMP map request as an ArrayBuffer + // Map internal port 55555 to external port 55555 w/ 120 sec lifetime + var pmpBuffer = new ArrayBuffer(12); + var pmpView = new DataView(pmpBuffer); + // Version and OP fields (1 byte each) + pmpView.setInt8(0, 0); + pmpView.setInt8(1, 1); + // Reserved, internal port, external port fields (2 bytes each) + pmpView.setInt16(2, 0, false); + pmpView.setInt16(4, 55555, false); + pmpView.setInt16(6, 55555, false); + // Mapping lifetime field (4 bytes) + pmpView.setInt32(8, 120, false); + + socket.sendTo(pmpBuffer, routerIp, 5351); + }).catch(R); }); // Give _probePmpSupport 2 seconds before timing out @@ -923,55 +900,51 @@ export function probePcpSupport(routerIp:string, privateIp:string) :Promise { - if (result != 0) { - R(new Error('Failed to bind to a port: Err= ' + result)); - } - - // Create the PCP MAP request as an ArrayBuffer - // Map internal port 55556 to external port 55556 w/ 120 sec lifetime - var pcpBuffer = new ArrayBuffer(60); - var pcpView = new DataView(pcpBuffer); - // Version field (1 byte) - pcpView.setInt8(0, 0b00000010); - // R and Opcode fields (1 bit + 7 bits) - pcpView.setInt8(1, 0b00000001); - // Reserved field (2 bytes) - pcpView.setInt16(2, 0, false); - // Requested lifetime (4 bytes) - pcpView.setInt32(4, 120, false); - // Client IP address (128 bytes; we use the IPv4 -> IPv6 mapping) - pcpView.setInt32(8, 0, false); - pcpView.setInt32(12, 0, false); - pcpView.setInt16(16, 0, false); - pcpView.setInt16(18, 0xffff, false); - // Start of IPv4 octets of the client's private IP - var ipOctets = ipaddr.IPv4.parse(privateIp).octets; - pcpView.setInt8(20, ipOctets[0]); - pcpView.setInt8(21, ipOctets[1]); - pcpView.setInt8(22, ipOctets[2]); - pcpView.setInt8(23, ipOctets[3]); - // Mapping Nonce (12 bytes) - pcpView.setInt32(24, randInt(0, 0xffffffff), false); - pcpView.setInt32(28, randInt(0, 0xffffffff), false); - pcpView.setInt32(32, randInt(0, 0xffffffff), false); - // Protocol (1 byte) - pcpView.setInt8(36, 17); - // Reserved (3 bytes) - pcpView.setInt16(37, 0, false); - pcpView.setInt8(39, 0); - // Internal and external ports - pcpView.setInt16(40, 55556, false); - pcpView.setInt16(42, 55556, false); - // External IP address (128 bytes; we use the all-zero IPv4 -> IPv6 mapping) - pcpView.setFloat64(44, 0, false); - pcpView.setInt16(52, 0, false); - pcpView.setInt16(54, 0xffff, false); - pcpView.setInt32(56, 0, false); - - socket.sendTo(pcpBuffer, routerIp, 5351); - }); + socket.bind('0.0.0.0', 0). + then(() => { + // Create the PCP MAP request as an ArrayBuffer + // Map internal port 55556 to external port 55556 w/ 120 sec lifetime + var pcpBuffer = new ArrayBuffer(60); + var pcpView = new DataView(pcpBuffer); + // Version field (1 byte) + pcpView.setInt8(0, 0b00000010); + // R and Opcode fields (1 bit + 7 bits) + pcpView.setInt8(1, 0b00000001); + // Reserved field (2 bytes) + pcpView.setInt16(2, 0, false); + // Requested lifetime (4 bytes) + pcpView.setInt32(4, 120, false); + // Client IP address (128 bytes; we use the IPv4 -> IPv6 mapping) + pcpView.setInt32(8, 0, false); + pcpView.setInt32(12, 0, false); + pcpView.setInt16(16, 0, false); + pcpView.setInt16(18, 0xffff, false); + // Start of IPv4 octets of the client's private IP + var ipOctets = ipaddr.IPv4.parse(privateIp).octets; + pcpView.setInt8(20, ipOctets[0]); + pcpView.setInt8(21, ipOctets[1]); + pcpView.setInt8(22, ipOctets[2]); + pcpView.setInt8(23, ipOctets[3]); + // Mapping Nonce (12 bytes) + pcpView.setInt32(24, randInt(0, 0xffffffff), false); + pcpView.setInt32(28, randInt(0, 0xffffffff), false); + pcpView.setInt32(32, randInt(0, 0xffffffff), false); + // Protocol (1 byte) + pcpView.setInt8(36, 17); + // Reserved (3 bytes) + pcpView.setInt16(37, 0, false); + pcpView.setInt8(39, 0); + // Internal and external ports + pcpView.setInt16(40, 55556, false); + pcpView.setInt16(42, 55556, false); + // External IP address (128 bytes; we use the all-zero IPv4 -> IPv6 mapping) + pcpView.setFloat64(44, 0, false); + pcpView.setInt16(52, 0, false); + pcpView.setInt16(54, 0xffff, false); + pcpView.setInt32(56, 0, false); + + socket.sendTo(pcpBuffer, routerIp, 5351); + }).catch(R); }); // Give _probePcpSupport 2 seconds before timing out @@ -1007,12 +980,8 @@ function sendSsdpRequest(privateIp:string) :Promise { }); // Bind a socket and send the SSDP request - socket.bind(privateIp, 0). - then((result:number) => { - if (result != 0) { - R(new Error('Failed to bind to a port: Err= ' + result)); - } - + socket.bind('0.0.0.0', 0). + then(() => { // Construct and send a UPnP SSDP message var ssdpStr = 'M-SEARCH * HTTP/1.1\r\n' + 'HOST: 239.255.255.250:1900\r\n' + @@ -1021,7 +990,7 @@ function sendSsdpRequest(privateIp:string) :Promise { 'ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1'; var ssdpBuffer = arraybuffers.stringToArrayBuffer(ssdpStr); socket.sendTo(ssdpBuffer, '239.255.255.250', 1900); - }); + }).catch(R); }); // Give _sendSsdpRequest 1 second before timing out diff --git a/src/generic_core/globals.ts b/src/generic_core/globals.ts index 9d097c62e2..8d80abf46f 100644 --- a/src/generic_core/globals.ts +++ b/src/generic_core/globals.ts @@ -42,7 +42,8 @@ export var settings :uproxy_core_api.GlobalSettings = { splashState: 0, statsReportingEnabled: false, consoleFilter: loggingTypes.Level.warn, - language: 'en' + language: 'en', + force_message_version: 0 // zero means "don't override" }; export var natType :string = ''; @@ -73,4 +74,10 @@ export var loadSettings :Promise = log.info('No global settings loaded', e.message); }); +// Client version to run as, which is globals.MESSAGE_VERSION unless +// overridden in advanced settings. +export var effectiveMessageVersion = () : number => { + return settings.force_message_version || MESSAGE_VERSION; +} + export var metrics = new metrics_module.Metrics(storage); diff --git a/src/generic_core/local-instance.ts b/src/generic_core/local-instance.ts index 0dd1a480b2..9df617f356 100644 --- a/src/generic_core/local-instance.ts +++ b/src/generic_core/local-instance.ts @@ -109,7 +109,7 @@ import storage = globals.storage; } public saveToStorage = () :Promise => { - return storage.save(this.getStorePath(), this.currentState()) + return storage.save(this.getStorePath(), this.currentState()) .catch((e:Error) => { log.error('Could not save new LocalInstance: ', this.instanceId, e.toString()); diff --git a/src/generic_core/remote-connection.spec.ts b/src/generic_core/remote-connection.spec.ts index fb6074243b..d1dfef43cc 100644 --- a/src/generic_core/remote-connection.spec.ts +++ b/src/generic_core/remote-connection.spec.ts @@ -51,7 +51,8 @@ describe('remote_connection.RemoteConnection', () => { spyOn(rtc_to_net, 'RtcToNet').and.returnValue(rtcToNet); updateSpy = jasmine.createSpy('updateSpy'); - connection = new remote_connection.RemoteConnection(updateSpy); + connection = new remote_connection.RemoteConnection(updateSpy, + 'the userId'); }); describe('client setup', () => { diff --git a/src/generic_core/remote-connection.ts b/src/generic_core/remote-connection.ts index 69f429d0d9..dc9dedf2d2 100644 --- a/src/generic_core/remote-connection.ts +++ b/src/generic_core/remote-connection.ts @@ -69,7 +69,8 @@ var generateProxyingSessionId_ = (): string => { private proxyingId_: string; constructor( - sendUpdate :(x :uproxy_core_api.Update, data?:Object) => void + sendUpdate :(x :uproxy_core_api.Update, data?:Object) => void, + private userId_?:string ) { this.sendUpdate_ = sendUpdate; this.resetSharerCreated(); @@ -142,7 +143,7 @@ var generateProxyingSessionId_ = (): string => { pc = bridge.best('rtctonet', config); } - this.rtcToNet_ = new rtc_to_net.RtcToNet(); + this.rtcToNet_ = new rtc_to_net.RtcToNet(this.userId_); this.rtcToNet_.start({ allowNonUnicast: globals.settings.allowNonUnicast }, pc); @@ -273,21 +274,30 @@ var generateProxyingSessionId_ = (): string => { }; var pc: peerconnection.PeerConnection; - if (remoteVersion === 1) { - log.debug('peer is running client version 1, using old peerconnection'); - pc = new peerconnection.PeerConnectionClass( - freedom['core.rtcpeerconnection'](config), - 'sockstortc'); - } else if (remoteVersion === 2) { - log.debug('peer is running client version 2, using bridge without obfuscation'); - pc = bridge.preObfuscation('sockstortc', config); - } else if (remoteVersion === 3) { - log.debug('peer is running client version 3, using bridge with basicObfuscation'); - pc = bridge.basicObfuscation('sockstortc', config); - } else { - log.debug('peer is running client version >=4, using holographic ICE'); - pc = bridge.best('sockstortc', config); - } + + var localVersion = globals.effectiveMessageVersion(); + var commonVersion = Math.min(localVersion, remoteVersion); + log.info('lowest shared client version is %1 (me: %2, peer: %3)', + commonVersion, localVersion, remoteVersion); + switch (commonVersion) { + case 1: + log.debug('using old peerconnection'); + pc = new peerconnection.PeerConnectionClass( + freedom['core.rtcpeerconnection'](config), + 'sockstortc'); + break; + case 2: + log.debug('using bridge without obfuscation'); + pc = bridge.preObfuscation('sockstortc', config); + break; + case 3: + log.debug('using bridge with basicObfuscation'); + pc = bridge.basicObfuscation('sockstortc', config); + break; + default: + log.debug('using holographic ICE'); + pc = bridge.best('sockstortc', config); + } return this.socksToRtc_.start(tcpServer, pc).then( (endpoint :net.Endpoint) => { diff --git a/src/generic_core/remote-instance.spec.ts b/src/generic_core/remote-instance.spec.ts index b86773c8d2..f1b329c0a6 100644 --- a/src/generic_core/remote-instance.spec.ts +++ b/src/generic_core/remote-instance.spec.ts @@ -61,13 +61,11 @@ describe('remote_instance.RemoteInstance', () => { }); describe('storage', () => { var realStorage = new local_storage.Storage; - var saved :Promise; var instance0 :remote_instance.RemoteInstance; it('fresh instance has no state', (done) => { globals.storage.save = function(key :string, value :Object) { - saved = realStorage.save(key, value); - return saved; + return realStorage.save(key, value); }; instance0 = new remote_instance.RemoteInstance(user, 'instanceId'); instance0.onceLoaded.then(() => { diff --git a/src/generic_core/remote-instance.ts b/src/generic_core/remote-instance.ts index 4f886d0e87..0a6d9f514b 100644 --- a/src/generic_core/remote-instance.ts +++ b/src/generic_core/remote-instance.ts @@ -105,7 +105,8 @@ export var remoteProxyInstance :RemoteInstance = null; // The User which this instance belongs to. public user :remote_user.User, public instanceId :string) { - this.connection_ = new remote_connection.RemoteConnection(this.handleConnectionUpdate_); + this.connection_ = new remote_connection.RemoteConnection( + this.handleConnectionUpdate_, this.user.userId); storage.load(this.getStorePath()) .then((state:RemoteInstanceState) => { @@ -343,8 +344,8 @@ export var remoteProxyInstance :RemoteInstance = null; private saveToStorage = () => { return this.onceLoaded.then(() => { var state = this.currentState(); - return storage.save(this.getStorePath(), state) - .then((old) => { + return storage.save(this.getStorePath(), state) + .then(() => { log.debug('Saved instance to storage', this.instanceId); }).catch((e) => { log.error('Failed saving instance to storage', this.instanceId, e.stack); diff --git a/src/generic_core/remote-user.spec.ts b/src/generic_core/remote-user.spec.ts index caf25cec7d..247ed0d512 100644 --- a/src/generic_core/remote-user.spec.ts +++ b/src/generic_core/remote-user.spec.ts @@ -182,11 +182,10 @@ describe('remote_user.User', () => { describe('client <---> instance', () => { it('syncs clientId <--> instanceId mapping', (done) => { var realStorage = new local_storage.Storage; - var saved :Promise; storage.save = function(key :string, value :Object) { - saved = realStorage.save(key, value); - return saved; + return realStorage.save(key, value); }; + spyOn(storage, 'save').and.callThrough(); expect(user.instanceToClient('fakeinstance')).toBeUndefined(); expect(user.clientToInstance('fakeclient')).toBeUndefined(); user.syncInstance_('fakeclient', instanceHandshake, @@ -195,7 +194,7 @@ describe('remote_user.User', () => { expect(user.clientToInstance('fakeclient')).toEqual('fakeinstance'); instance = user.getInstance('fakeinstance'); expect(instance).toBeDefined(); - expect(saved).toBeDefined(); + expect(storage.save).toHaveBeenCalled(); done(); }); }); diff --git a/src/generic_core/remote-user.ts b/src/generic_core/remote-user.ts index a715ff40e0..0c279b1fcc 100644 --- a/src/generic_core/remote-user.ts +++ b/src/generic_core/remote-user.ts @@ -447,7 +447,7 @@ var log :logging.Log = new logging.Log('remote-user'); this.onceLoaded.then(() => { if (!this.ignoreUser_()) { var state = this.currentState(); - storage.save(this.getStorePath(), state).catch(() => { + storage.save(this.getStorePath(), state).catch(() => { log.error('Could not save user to storage'); }); } diff --git a/src/generic_core/social.spec.ts b/src/generic_core/social.spec.ts index 394767b71d..c35e48e2b1 100644 --- a/src/generic_core/social.spec.ts +++ b/src/generic_core/social.spec.ts @@ -105,8 +105,8 @@ describe('social_network.FreedomNetwork', () => { }) }); - var promises :Promise[] = []; - promises.push(storage.save('mockmockmyself', { + var savedToStorage :Promise[] = []; + savedToStorage.push(storage.save('mockmockmyself', { instanceId: 'dummy-instance-id', keyHash: '', bytesReceived: 0, @@ -116,10 +116,10 @@ describe('social_network.FreedomNetwork', () => { localGettingFromRemote: social.GettingState.NONE, localSharingWithRemote: social.SharingState.NONE })); - promises.push(storage.save( + savedToStorage.push(storage.save( 'dummy-instance-id/roster/somefriend', '')); - Promise.all(promises).then(() => { + Promise.all(savedToStorage).then(() => { var loginPromise = network.login(false); return loginPromise; }).then(() => { diff --git a/src/generic_core/social.ts b/src/generic_core/social.ts index eadbd94c6a..e806e14109 100644 --- a/src/generic_core/social.ts +++ b/src/generic_core/social.ts @@ -565,7 +565,7 @@ export function notifyUI(networkName :string, userId :string) { var versionedMessage :social.VersionedPeerMessage = { type: message.type, data: message.data, - version: globals.MESSAGE_VERSION + version: globals.effectiveMessageVersion() }; var messageString = JSON.stringify(versionedMessage); log.info('sending message', { @@ -685,7 +685,7 @@ export function notifyUI(networkName :string, userId :string) { var versionedMessage :social.VersionedPeerMessage = { type: message.type, data: message.data, - version: globals.MESSAGE_VERSION + version: globals.effectiveMessageVersion() }; ui.update(uproxy_core_api.Update.MANUAL_NETWORK_OUTBOUND_MESSAGE, versionedMessage); diff --git a/src/generic_core/storage.ts b/src/generic_core/storage.ts index 54cd258b3e..ad33711152 100644 --- a/src/generic_core/storage.ts +++ b/src/generic_core/storage.ts @@ -9,101 +9,70 @@ import logging = require('../../../third_party/uproxy-lib/logging/logging'); -import Persistent = require('../interfaces/persistent'); - var log :logging.Log = new logging.Log('storage'); - // Platform-independent storage provider. - var fStorage :freedom_Storage = freedom['core.storage'](); +// Platform-independent storage provider. +var fStorage :freedom_Storage = freedom['core.storage'](); - // Set false elsewhere to disable log messages (ie. from jasmine) - export var DEBUG_STATESTORAGE = true; +// Set false elsewhere to disable log messages (ie. from jasmine) +export var DEBUG_STATESTORAGE = true; +/** + * Contains all state for uProxy's core. + */ +export class Storage { /** - * Contains all state for uProxy's core. + * Resets state, and clears local storage. */ - export class Storage { + public reset = () : Promise => { + return fStorage.clear().then(() => { + log.info('Cleared all keys from storage'); + }); + } - /** - * Resets state, and clears local storage. - */ - public reset = () : Promise => { - return fStorage.clear().then(() => { - log.info('Cleared all keys from storage'); - // TODO: Determine if we actually need any 'initial' state. - }); - } + // -------------------------------------------------------------------------- + // Promise-based wrappers for Freedom storage API to work with json instead + // of strings. - // -------------------------------------------------------------------------- - // Promise-based wrappers for Freedom storage API to work with json instead - // of strings. - - /** - * Promise loading a key from storage, as a JSON object. - * Use Generic to indicate the type of the returned object. - * If the key does not exist, rejects the promise. - * - * TODO: Consider using a storage provider that works with JSON. - * TODO: Really reject the promise! - */ - public load(key :string) : Promise { - log.debug('loading', key); - return fStorage.get(key).then((result :string) => { - if (typeof result === 'undefined' || result === null) { - return Promise.reject('non-existing key'); - } - log.debug('Loaded [%1]: %2', key, result); - return JSON.parse(result); - }); - } + /** + * Promise loading a key from storage, as a JSON object. + * Use Generic to indicate the type of the returned object. + * If the key does not exist, rejects the promise. + * + * TODO: Consider using a storage provider that works with JSON. + */ + public load(key :string) : Promise { + log.debug('loading', key); + return fStorage.get(key).then((result :string) => { + if (typeof result === 'undefined' || result === null) { + return Promise.reject('non-existing key'); + } + log.debug('Loaded [%1]: %2', key, result); + return JSON.parse(result); + }); + } - /** - * Promise saving a key-value pair to storage, fulfilled with the previous - * value of |key| if it existed (according to the freedom interface.) - */ - // TODO: should not return a value in the promise. Should be Promise - public save(key :string, val :T) : Promise { - log.debug('Saving to storage', { + /** + * Promise saving a key-value pair to storage, fulfilled with the previous + * value of |key| if it existed (according to the freedom interface.) + */ + public save(key :string, val :Object) :Promise { + log.debug('Saving to storage', { + key: key, + newVal: val + }); + return fStorage.set(key, JSON.stringify(val)).then((prev:string) => { + log.debug('Successfully saved to storage', { key: key, - newVal: val - }); - return fStorage.set(key, JSON.stringify(val)).then((prev:string) => { - log.debug('Successfully saved to storage', { - key: key, - oldVal: prev - }); - if (!prev) { - return undefined; - } - return JSON.parse(prev); - }).catch((e) => { - log.error('Save operation failed', e.message); - return {}; + oldVal: prev }); - } + }).catch((e) => { + log.error('Save operation failed', e.message); + return Promise.reject(e); + }); + } - public keys = () : Promise => { - return fStorage.keys(); - } - - // -------------------------------------------------------------------------- - // Options - // TODO: Move options to its own class and fix it. - // -------------------------------------------------------------------------- - /* - public saveOptionsToStorage = () : Promise => { - return this.save( - C.StateEntries.OPTIONS, - null); - // restrictKeys(C.DEFAULT_SAVE_STATE.options, this.state.options)); - } - - public loadOptionsFromStorage = () : Promise => { - return this.load(C.StateEntries.OPTIONS, {}).then((loadedOptions) => { - dbg('loaded options: ' + loadedOptions); - // this.state.options = - // restrictKeys(cloneDeep(C.DEFAULT_LOAD_STATE.options), loadedOptions); - }); - } - */ - } // class Storage + public keys = () : Promise => { + return fStorage.keys(); + } +} // class Storage diff --git a/src/generic_core/uproxy_core.ts b/src/generic_core/uproxy_core.ts index 03deb6f568..af97135939 100644 --- a/src/generic_core/uproxy_core.ts +++ b/src/generic_core/uproxy_core.ts @@ -162,7 +162,7 @@ export class uProxyCore implements uproxy_core_api.CoreApi { if (newSettings.stunServers.length === 0) { newSettings.stunServers = globals.DEFAULT_STUN_SERVERS; } - globals.storage.save('globalSettings', newSettings) + globals.storage.save('globalSettings', newSettings) .catch((e) => { log.error('Could not save globalSettings to storage', e.stack); }); @@ -198,6 +198,7 @@ export class uProxyCore implements uproxy_core_api.CoreApi { loggingTypes.Destination.console, globals.settings.consoleFilter); globals.settings.language = newSettings.language; + globals.settings.force_message_version = newSettings.force_message_version; } public getFullState = () :Promise => { @@ -241,7 +242,7 @@ export class uProxyCore implements uproxy_core_api.CoreApi { remoteProxyInstance = null; } - return copyPasteConnection.startGet(globals.MESSAGE_VERSION); + return copyPasteConnection.startGet(globals.effectiveMessageVersion()); } public stopCopyPasteGet = () :Promise => { @@ -250,7 +251,7 @@ export class uProxyCore implements uproxy_core_api.CoreApi { public startCopyPasteShare = () => { this.copyPasteSharingMessages_ = []; - copyPasteConnection.startShare(globals.MESSAGE_VERSION); + copyPasteConnection.startShare(globals.effectiveMessageVersion()); } public stopCopyPasteShare = () :Promise => { diff --git a/src/generic_ui/locales/en/messages.json b/src/generic_ui/locales/en/messages.json index 79bd2c3c1b..8710e4a2da 100644 --- a/src/generic_ui/locales/en/messages.json +++ b/src/generic_ui/locales/en/messages.json @@ -35,6 +35,10 @@ "description": "Submit feedback to the uProxy team.", "message": "Submit Feedback" }, + "sendingFeedback": { + "description": "Message in dialog indicating feedback is being sent to the uProxy team.", + "message": "Sending feedback" + }, "getHelp": { "description": "Get help from the frequently asked questions page.", "message": "Get Help" diff --git a/src/generic_ui/polymer/feedback.ts b/src/generic_ui/polymer/feedback.ts index 2966ebd267..a9e307ea2f 100644 --- a/src/generic_ui/polymer/feedback.ts +++ b/src/generic_ui/polymer/feedback.ts @@ -1,16 +1,24 @@ /// +import uproxy_core_api = require('../../interfaces/uproxy_core_api'); + Polymer({ email: '', feedback: '', logs: '', + feedbackType: '', close: function() { this.$.feedbackPanel.close(); }, - open: function(e :Event, detail :{ includeLogs: boolean }) { - if (detail && detail.includeLogs) { + open: function(e:Event, data?:{ + includeLogs: boolean; + feedbackType: uproxy_core_api.UserFeedbackType; + }) { + if (data && data.includeLogs) { this.$.logCheckbox.checked = true; } + this.feedbackType = (data && data.feedbackType) ? data.feedbackType : + uproxy_core_api.UserFeedbackType.USER_INITIATED; this.$.feedbackPanel.open(); }, sendFeedback: function() { @@ -19,7 +27,8 @@ Polymer({ email: this.email, feedback: this.feedback, logs: this.$.logCheckbox.checked, - browserInfo: navigator.userAgent + browserInfo: navigator.userAgent, + feedbackType: this.feedbackType }).then(() => { // Reset the placeholders, which seem to be cleared after the // user types input in the input fields. diff --git a/src/generic_ui/polymer/troubleshoot.ts b/src/generic_ui/polymer/troubleshoot.ts index 31cedcf095..ac759c68cc 100644 --- a/src/generic_ui/polymer/troubleshoot.ts +++ b/src/generic_ui/polymer/troubleshoot.ts @@ -1,3 +1,5 @@ +import uproxy_core_api = require('../../interfaces/uproxy_core_api'); + Polymer({ analyzingNetwork: false, analyzedNetwork: false, @@ -12,7 +14,12 @@ Polymer({ this.$.troubleshootDialog.open(); }, submitFeedback: function() { - this.fire('core-signal', {name: 'open-feedback', data: {includeLogs: this.analyzedNetwork}}); + this.fire('core-signal', { + name: 'open-feedback', data: { + includeLogs: this.analyzedNetwork, + feedbackType: uproxy_core_api.UserFeedbackType.PROXYING_FAILURE + } + }); this.close(); }, getNatType: function() { diff --git a/src/generic_ui/scripts/translator.ts b/src/generic_ui/scripts/translator.ts index 225f036dfc..7d680c7522 100644 --- a/src/generic_ui/scripts/translator.ts +++ b/src/generic_ui/scripts/translator.ts @@ -34,7 +34,8 @@ function createI18nDictionary(sourceFile :MessageResource) : IResourceStoreKey { window.i18nResources = {}; i18n.init({ - resStore: window.i18nResources + resStore: window.i18nResources, + fallbackLng: 'en' }); i18n.addResources('en', 'translation', createI18nDictionary(english_source)); diff --git a/src/generic_ui/scripts/ui.spec.ts b/src/generic_ui/scripts/ui.spec.ts index a6147f370e..1719670525 100644 --- a/src/generic_ui/scripts/ui.spec.ts +++ b/src/generic_ui/scripts/ui.spec.ts @@ -43,7 +43,7 @@ describe('UI.UserInterface', () => { }); mockBrowserApi = jasmine.createSpyObj('browserApi', - ['setIcon', 'startUsingProxy', 'stopUsingProxy', 'openTab', 'showNotification', 'on']); + ['setIcon', 'startUsingProxy', 'stopUsingProxy', 'openTab', 'showNotification', 'on', 'handlePopupLaunch']); ui = new user_interface.UserInterface(mockCore, mockBrowserApi); spyOn(console, 'log'); }); @@ -99,11 +99,11 @@ describe('UI.UserInterface', () => { it('Adding a user with no information is categorized as untrusted', () => { ui.syncUser(getUserAndInstance('testUserId', 'Alice', 'instance1')); - var network = user_interface.model.getNetwork('testNetwork'); - var user = user_interface.model.getUser(network, 'testUsedId'); + var network = ui.model.getNetwork('testNetwork'); + var user = ui.model.getUser(network, 'testUsedId'); expect(user).toBeDefined(); - var contacts = user_interface.model.contacts; + var contacts = ui.model.contacts; expect(contacts.getAccessContacts.trustedUproxy.length).toEqual(0); expect(contacts.getAccessContacts.untrustedUproxy.length).toEqual(1); @@ -118,9 +118,9 @@ describe('UI.UserInterface', () => { afterEach(logout); it('Network visible in model', () => { - expect(user_interface.model.onlineNetworks.length).toEqual(1); + expect(ui.model.onlineNetworks.length).toEqual(1); - var network = user_interface.model.getNetwork('testNetwork'); + var network = ui.model.getNetwork('testNetwork'); expect(network.name).toEqual('testNetwork'); expect(network.userId).toEqual('fakeUser'); }); @@ -138,7 +138,7 @@ describe('UI.UserInterface', () => { } }); - var network = user_interface.model.getNetwork('testNetwork'); + var network = ui.model.getNetwork('testNetwork'); expect(network.userName).toEqual('testName'); expect(network.imageData).toEqual('imageData'); @@ -151,7 +151,7 @@ describe('UI.UserInterface', () => { it('Clears fields when network goes offline', () => { logout(); - expect(user_interface.model.onlineNetworks.length).toEqual(0); + expect(ui.model.onlineNetworks.length).toEqual(0); }); }); diff --git a/src/generic_ui/scripts/ui.ts b/src/generic_ui/scripts/ui.ts index 4255127d13..ab8fd4250f 100644 --- a/src/generic_ui/scripts/ui.ts +++ b/src/generic_ui/scripts/ui.ts @@ -59,7 +59,8 @@ export class Model { allowNonUnicast: false, statsReportingEnabled: false, consoleFilter: 2, // loggingTypes.Level.warn - language: 'en' + language: 'en', + force_message_version: 0 }; public reconnecting = false; @@ -71,7 +72,18 @@ export class Model { return _.find(this.onlineNetworks, { name: networkName }); } - public removeNetwork = (networkName :string) => { + public removeNetwork = (networkName :string, userId :string) => { + var network = this.getNetwork(networkName, userId); + + for (var otherUserId in network.roster) { + var user = this.getUser(network, otherUserId); + var userCategories = user.getCategories(); + categorizeUser(user, this.contacts.getAccessContacts, + userCategories.getTab, null); + categorizeUser(user, this.contacts.shareAccessContacts, + userCategories.shareTab, null); + } + _.remove(this.onlineNetworks, { name: networkName }); } @@ -94,9 +106,6 @@ export class Model { } } -// Singleton model for data bindings. -export var model = new Model(); - export interface ContactCategory { [type :string] :User[]; pending :User[]; @@ -180,6 +189,9 @@ export class UserInterface implements ui_constants.UiApi { public unableToGet :boolean = false; public unableToShare :boolean = false; + // ID of the most recent failed proxying attempt. + public proxyingId: string; + // is a proxy currently set private proxySet_ :boolean = false; // Must be included in Chrome extension manifest's list of permissions. @@ -197,6 +209,8 @@ export class UserInterface implements ui_constants.UiApi { public i18n_t :Function = translator_module.i18n_t; public i18n_setLng :Function = translator_module.i18n_setLng; + public model = new Model(); + /** * UI must be constructed with hooks to Notifications and Core. * Upon construction, the UI installs update handlers on core. @@ -206,7 +220,7 @@ export class UserInterface implements ui_constants.UiApi { public browserApi :BrowserAPI) { // TODO: Determine the best way to describe view transitions. this.view = ui_constants.View.SPLASH; // Begin at the splash intro. - this.i18n_setLng(model.globalSettings.language); + this.i18n_setLng(this.model.globalSettings.language); var firefoxMatches = navigator.userAgent.match(/Firefox\/(\d+)/); if (firefoxMatches) { @@ -349,6 +363,7 @@ export class UserInterface implements ui_constants.UiApi { name: info.name }); this.unableToShare = true; + this.proxyingId = info.proxyingId; }); core.onUpdate(uproxy_core_api.Update.FAILED_TO_GET, @@ -360,6 +375,7 @@ export class UserInterface implements ui_constants.UiApi { }); this.instanceTryingToGetAccessFrom = null; this.unableToGet = true; + this.proxyingId = info.proxyingId; this.bringUproxyToFront(); }); @@ -373,7 +389,9 @@ export class UserInterface implements ui_constants.UiApi { browserApi.on('notificationClicked', this.handleNotificationClick); browserApi.on('proxyDisconnected', this.proxyDisconnected); - core.getFullState().then(this.updateInitialState); + core.getFullState() + .then(this.updateInitialState) + .then(this.browserApi.handlePopupLaunch); } // Because of an observer (in root.ts) watching the value of @@ -406,21 +424,21 @@ export class UserInterface implements ui_constants.UiApi { var data = JSON.parse(tag); if (data.network && data.user) { - var network = model.getNetwork(data.network); + var network = this.model.getNetwork(data.network); if (network) { - var contact = model.getUser(network, data.user); + var contact = this.model.getUser(network, data.user); } } if (data.mode === 'get') { - model.globalSettings.mode = ui_constants.Mode.GET; - this.core.updateGlobalSettings(model.globalSettings); + this.model.globalSettings.mode = ui_constants.Mode.GET; + this.core.updateGlobalSettings(this.model.globalSettings); if (contact) { contact.getExpanded = true; } } else if (data.mode === 'share' && !this.isSharingDisabled) { - model.globalSettings.mode = ui_constants.Mode.SHARE; - this.core.updateGlobalSettings(model.globalSettings); + this.model.globalSettings.mode = ui_constants.Mode.SHARE; + this.core.updateGlobalSettings(this.model.globalSettings); if (contact) { contact.shareExpanded = true; } @@ -469,7 +487,7 @@ export class UserInterface implements ui_constants.UiApi { var expectedType :social.PeerMessageType; console.log('received url data from browser'); - if (model.onlineNetworks.length > 0) { + if (this.model.onlineNetworks.length > 0) { console.log('Ignoring URL since we have an active network'); this.copyPasteError = ui_constants.CopyPasteError.LOGGED_IN; return; @@ -652,7 +670,7 @@ export class UserInterface implements ui_constants.UiApi { this.browserApi.setIcon(Constants.GETTING_ICON); } else if (isGiving) { this.browserApi.setIcon(Constants.SHARING_ICON); - } else if (model.onlineNetworks.length > 0) { + } else if (this.model.onlineNetworks.length > 0) { this.browserApi.setIcon(Constants.DEFAULT_ICON); } else { this.browserApi.setIcon(Constants.LOGGED_OUT_ICON); @@ -679,7 +697,7 @@ export class UserInterface implements ui_constants.UiApi { * Synchronize a new network to be visible on this UI. */ private syncNetwork_ = (networkMsg :social.NetworkMessage) => { - var existingNetwork = model.getNetwork(networkMsg.name, networkMsg.userId); + var existingNetwork = this.model.getNetwork(networkMsg.name, networkMsg.userId); if (networkMsg.online) { if (!existingNetwork) { @@ -691,19 +709,11 @@ export class UserInterface implements ui_constants.UiApi { userName: networkMsg.userName, imageData: networkMsg.imageData }; - model.onlineNetworks.push(existingNetwork); + this.model.onlineNetworks.push(existingNetwork); } } else { if (existingNetwork) { - for (var userId in existingNetwork.roster) { - var user = existingNetwork.roster[userId]; - var userCategories = user.getCategories(); - this.categorizeUser_(user, model.contacts.getAccessContacts, - userCategories.getTab, null); - this.categorizeUser_(user, model.contacts.shareAccessContacts, - userCategories.shareTab, null); - } - model.removeNetwork(networkMsg.name); + this.model.removeNetwork(networkMsg.name, networkMsg.userId); if (!existingNetwork.logoutExpected && (networkMsg.name === 'Google' || networkMsg.name === 'Facebook') && @@ -716,7 +726,7 @@ export class UserInterface implements ui_constants.UiApi { } this.showNotification(this.i18n_t('loggedOut', {network: networkMsg.name})); - if (!model.onlineNetworks.length) { + if (!this.model.onlineNetworks.length) { this.view = ui_constants.View.SPLASH; } } @@ -727,7 +737,7 @@ export class UserInterface implements ui_constants.UiApi { } private syncUserSelf_ = (payload :social.UserData) => { - var network = model.getNetwork(payload.network); + var network = this.model.getNetwork(payload.network); if (!network) { console.error('uproxy_core_api.Update.USER_SELF message for invalid network', payload.network); @@ -743,7 +753,7 @@ export class UserInterface implements ui_constants.UiApi { * Synchronize data about some friend. */ public syncUser = (payload :social.UserData) => { - var network = model.getNetwork(payload.network); + var network = this.model.getNetwork(payload.network); if (!network) { return; } @@ -753,7 +763,7 @@ export class UserInterface implements ui_constants.UiApi { // Update / create if necessary a user, both in the network-specific // roster and the global roster. var user :User; - user = model.getUser(network, profile.userId); + user = this.model.getUser(network, profile.userId); var oldUserCategories :UserCategories = { getTab: null, shareTab: null @@ -791,37 +801,14 @@ export class UserInterface implements ui_constants.UiApi { var newUserCategories = user.getCategories(); // Update the user's category in both get and share tabs. - this.categorizeUser_(user, model.contacts.getAccessContacts, + categorizeUser(user, this.model.contacts.getAccessContacts, oldUserCategories.getTab, newUserCategories.getTab); - this.categorizeUser_(user, model.contacts.shareAccessContacts, + categorizeUser(user, this.model.contacts.shareAccessContacts, oldUserCategories.shareTab, newUserCategories.shareTab); console.log('Synchronized user.', user); }; - private categorizeUser_ = (user :User, contacts :ContactCategory, oldCategory :string, newCategory :string) => { - if (oldCategory === newCategory) { - // no need to do any work if nothing changed - return; - } - - if (oldCategory) { - // remove user from old category - var oldCategoryArray = contacts[oldCategory]; - for (var i = 0; i < oldCategoryArray.length; ++i) { - if (oldCategoryArray[i] == user) { - oldCategoryArray.splice(i, 1); - break; - } - } - } - - if (newCategory) { - // add user to new category - contacts[newCategory].push(user); - } - } - public openTab = (url :string) => { this.browserApi.openTab(url); } @@ -838,7 +825,7 @@ export class UserInterface implements ui_constants.UiApi { } public logout = (networkInfo :social.SocialNetworkInfo) : Promise => { - var network = model.getNetwork(networkInfo.name); + var network = this.model.getNetwork(networkInfo.name); if (network) { // if we know about the network, record that we expect this logout to // happen @@ -852,14 +839,14 @@ export class UserInterface implements ui_constants.UiApi { } public reconnect = (network :string) => { - model.reconnecting = true; + this.model.reconnecting = true; var pingUrl = network == 'Facebook' ? 'https://graph.facebook.com' : 'https://www.googleapis.com'; this.core.pingUntilOnline(pingUrl).then(() => { // Ensure that the user is still attempting to reconnect (i.e. they // haven't clicked to stop reconnecting while we were waiting for the // ping response). - if (model.reconnecting) { + if (this.model.reconnecting) { this.core.login({network: network, reconnect: true}).then(() => { this.stopReconnect(); }).catch((e) => { @@ -868,7 +855,7 @@ export class UserInterface implements ui_constants.UiApi { this.showNotification( this.i18n_t('loggedOut', { network: network })); - if (!model.onlineNetworks.length) { + if (!this.model.onlineNetworks.length) { this.view = ui_constants.View.SPLASH; } }); @@ -877,14 +864,14 @@ export class UserInterface implements ui_constants.UiApi { } public stopReconnect = () => { - model.reconnecting = false; + this.model.reconnecting = false; } private cloudfrontDomains_ = [ "d1wtwocg4wx1ih.cloudfront.net" ] - public postToCloudfrontSite = (payload :any, cloudfrontPath :string, + public postToCloudfrontSite = (payload :Object, cloudfrontPath :string, maxAttempts ?:number) : Promise => { console.log('postToCloudfrontSite: ', payload, cloudfrontPath); @@ -922,30 +909,33 @@ export class UserInterface implements ui_constants.UiApi { var payload = { email: feedback.email, feedback: feedback.feedback, - logs: logs + logs: logs, + feedbackType: uproxy_core_api.UserFeedbackType[feedback.feedbackType], + proxyingId: this.proxyingId }; + return this.postToCloudfrontSite(payload, 'submit-feedback'); }); } public setMode = (mode :ui_constants.Mode) => { - model.globalSettings.mode = mode; - this.core.updateGlobalSettings(model.globalSettings); + this.model.globalSettings.mode = mode; + this.core.updateGlobalSettings(this.model.globalSettings); } public updateLanguage = (newLanguage :string) => { - model.globalSettings.language = newLanguage; - this.core.updateGlobalSettings(model.globalSettings); + this.model.globalSettings.language = newLanguage; + this.core.updateGlobalSettings(this.model.globalSettings); this.i18n_setLng(newLanguage); } public updateInitialState = (state :uproxy_core_api.InitialState) => { console.log('Received uproxy_core_api.Update.INITIAL_STATE:', state); - model.networkNames = state.networkNames; - if (state.globalSettings.language !== model.globalSettings.language) { + this.model.networkNames = state.networkNames; + if (state.globalSettings.language !== this.model.globalSettings.language) { this.i18n_setLng(state.globalSettings.language); } - model.updateGlobalSettings(state.globalSettings); + this.model.updateGlobalSettings(state.globalSettings); // Maybe refactor this to be copyPasteState. this.copyPasteState = state.copyPasteState.connectionState; @@ -958,10 +948,10 @@ export class UserInterface implements ui_constants.UiApi { this.view = ui_constants.View.COPYPASTE; } - this.browserApi.fulfillLaunched(); + while (this.model.onlineNetworks.length > 0) { + var toRemove = this.model.onlineNetworks[0]; - while(model.onlineNetworks.length > 0) { - model.onlineNetworks.pop(); + this.model.removeNetwork(toRemove.name, toRemove.userId); } for (var network in state.onlineNetworks) { @@ -981,7 +971,7 @@ export class UserInterface implements ui_constants.UiApi { } private addOnlineNetwork_ = (networkState :social.NetworkState) => { - model.onlineNetworks.push({ + this.model.onlineNetworks.push({ name: networkState.name, userId: networkState.profile.userId, userName: networkState.profile.name, @@ -994,4 +984,28 @@ export class UserInterface implements ui_constants.UiApi { this.syncUser(networkState.roster[userId]); } } -} // class UserInterface +} // class UserInterface + +// non-exported method to handle categorizing users +function categorizeUser(user :User, contacts :ContactCategory, oldCategory :string, newCategory :string) { + if (oldCategory === newCategory) { + // no need to do any work if nothing changed + return; + } + + if (oldCategory) { + // remove user from old category + var oldCategoryArray = contacts[oldCategory]; + for (var i = 0; i < oldCategoryArray.length; ++i) { + if (oldCategoryArray[i] == user) { + oldCategoryArray.splice(i, 1); + break; + } + } + } + + if (newCategory) { + // add user to new category + contacts[newCategory].push(user); + } +} diff --git a/src/generic_ui/scripts/user.spec.ts b/src/generic_ui/scripts/user.spec.ts index 663e087648..709fb85db4 100644 --- a/src/generic_ui/scripts/user.spec.ts +++ b/src/generic_ui/scripts/user.spec.ts @@ -13,7 +13,8 @@ describe('UI.User', () => { spyOn(console, 'log'); ui = jasmine.createSpyObj('UserInterface', ['showNotification']); var testNetwork :user_interface.Network = {name: 'testNetwork', userId: 'localUserId', roster: {}, logoutExpected: false}; - user_interface.model.onlineNetworks = [testNetwork]; + ui.model = new user_interface.Model(); + ui.model.onlineNetworks = [testNetwork]; sampleUser = new user.User('fakeuser', testNetwork, ui); sampleUser.update(makeUpdateMessage({})); diff --git a/src/interfaces/browser_api.ts b/src/interfaces/browser_api.ts index c8725329f2..1a7c535893 100644 --- a/src/interfaces/browser_api.ts +++ b/src/interfaces/browser_api.ts @@ -43,5 +43,6 @@ export interface BrowserAPI { on(name :'notificationClicked', callback :(tag :string) => void) :void; on(name :'proxyDisconnected', callback :Function) :void; - fulfillLaunched() :void; + // should be called when popup is launched and ready for use + handlePopupLaunch() :void; } diff --git a/src/interfaces/uproxy_core_api.ts b/src/interfaces/uproxy_core_api.ts index c71b8add03..1888d4bff9 100644 --- a/src/interfaces/uproxy_core_api.ts +++ b/src/interfaces/uproxy_core_api.ts @@ -8,10 +8,17 @@ import ui = require('./ui'); // --- Core <--> UI Interfaces --- export interface UserFeedback { - email :string; - feedback :string; - logs :boolean; - browserInfo :string; + email :string; + feedback :string; + logs :string; + browserInfo ?:string; + proxyingId ?:string; + feedbackType ?:UserFeedbackType; +} + +export enum UserFeedbackType { + USER_INITIATED = 0, + PROXYING_FAILURE = 1 } // Object containing description so it can be saved to storage. @@ -27,6 +34,7 @@ export interface GlobalSettings { splashState : number; consoleFilter :loggingTypes.Level; language :string; + force_message_version :number; } export interface InitialState { networkNames :string[]; diff --git a/src/mocks/rtc-to-net.ts b/src/mocks/rtc-to-net.ts index ca5e83ccbd..c2c40ecb0e 100644 --- a/src/mocks/rtc-to-net.ts +++ b/src/mocks/rtc-to-net.ts @@ -34,6 +34,4 @@ export class RtcToNetMock { // TODO implements rtc_to_net.RtcToNet { public toString = () => { } - - public startFromConfig = () => {} }