diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e58951412..9360c23fe 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -166,6 +166,11 @@ "description": "Only available on development modes, menu option to open up the standalone device setup sequence" }, + "connectingLoad": { + "message": "Connecting...", + "description": + "Message shown on the as a loading screen while we are connecting to something" + }, "loading": { "message": "Loading...", "description": @@ -1940,6 +1945,16 @@ "message": "Show QR code", "description": "Button action that the user can click to view their QR code" }, + "showAddServer": { + "message": "Add public server", + "description": + "Button action that the user can click to connect to a new public server" + }, + "addServerDialogTitle": { + "message": "Connect to new public server", + "description": + "Title for the dialog box used to connect to a new public server" + }, "seedViewTitle": { "message": @@ -1991,6 +2006,15 @@ "passwordsDoNotMatch": { "message": "Passwords do not match" }, + "publicChatExists": { + "message": "You are already connected to this public channel" + }, + "connectToServerFail": { + "message": "Failed to connect to server. Check URL" + }, + "connectToServerSuccess": { + "message": "Successfully connected to new public chat server" + }, "setPasswordFail": { "message": "Failed to set password" }, diff --git a/background.html b/background.html index acad1030c..84bebabbd 100644 --- a/background.html +++ b/background.html @@ -282,6 +282,29 @@ + + + + diff --git a/js/background.js b/js/background.js index 710471086..b002669dc 100644 --- a/js/background.js +++ b/js/background.js @@ -233,6 +233,9 @@ window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey); // singleton to interface the File server window.lokiFileServerAPI = new window.LokiFileServerAPI(ourKey); + await window.lokiFileServerAPI.establishConnection( + window.getDefaultFileServer() + ); // are there limits on tracking, is this unneeded? // window.mixpanel.track("Desktop boot"); window.lokiP2pAPI = new window.LokiP2pAPI(ourKey); @@ -749,6 +752,12 @@ } }); + Whisper.events.on('showAddServerDialog', async options => { + if (appView) { + appView.showAddServerDialog(options); + } + }); + Whisper.events.on('showQRDialog', async () => { if (appView) { const ourNumber = textsecure.storage.user.getNumber(); diff --git a/js/models/conversations.js b/js/models/conversations.js index de9c2216f..35ecae10c 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1421,7 +1421,7 @@ options.messageType = message.get('type'); options.isPublic = this.isPublic(); if (options.isPublic) { - options.publicSendData = this.getPublicSendData(); + options.publicSendData = await this.getPublicSendData(); } const groupNumbers = this.getRecipients(); @@ -2122,6 +2122,21 @@ }; }, // maybe "Backend" instead of "Source"? + async setPublicSource(newServer, newChannelId) { + if (!this.isPublic()) { + return; + } + if ( + this.get('server') !== newServer || + this.get('channelId') !== newChannelId + ) { + this.set({ server: newServer }); + this.set({ channelId: newChannelId }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + } + }, getPublicSource() { if (!this.isPublic()) { return null; @@ -2132,10 +2147,18 @@ conversationId: this.get('id'), }; }, - getPublicSendData() { - const serverAPI = lokiPublicChatAPI.findOrCreateServer( + async getPublicSendData() { + const serverAPI = await lokiPublicChatAPI.findOrCreateServer( this.get('server') ); + if (!serverAPI) { + window.log.warn( + `Failed to get serverAPI (${this.get('server')}) for conversation (${ + this.id + })` + ); + return null; + } const channelAPI = serverAPI.findOrCreateChannel( this.get('channelId'), this.id @@ -2397,6 +2420,9 @@ async deletePublicMessage(message) { const channelAPI = this.getPublicSendData(); + if (!channelAPI) { + return false; + } const success = await channelAPI.deleteMessage(message.getServerId()); if (success) { this.removeMessage(message.id); diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 909caf5a3..eb113d860 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -28,21 +28,32 @@ class LokiAppDotNetAPI extends EventEmitter { } // server getter/factory - findOrCreateServer(serverUrl) { + async findOrCreateServer(serverUrl) { let thisServer = this.servers.find( server => server.baseServerUrl === serverUrl ); if (!thisServer) { log.info(`LokiAppDotNetAPI creating ${serverUrl}`); thisServer = new LokiAppDotNetServerAPI(this, serverUrl); + const gotToken = await thisServer.getOrRefreshServerToken(); + if (!gotToken) { + log.warn(`Invalid server ${serverUrl}`); + return null; + } + log.info(`set token ${thisServer.token}`); + this.servers.push(thisServer); } return thisServer; } // channel getter/factory - findOrCreateChannel(serverUrl, channelId, conversationId) { - const server = this.findOrCreateServer(serverUrl); + async findOrCreateChannel(serverUrl, channelId, conversationId) { + const server = await this.findOrCreateServer(serverUrl); + if (!server) { + log.error(`Failed to create server for: ${serverUrl}`); + return null; + } return server.findOrCreateChannel(channelId, conversationId); } @@ -82,11 +93,6 @@ class LokiAppDotNetServerAPI { this.channels = []; this.tokenPromise = null; this.baseServerUrl = url; - const ref = this; - (async function justToEnableAsyncToGetToken() { - ref.token = await ref.getOrRefreshServerToken(); - log.info(`set token ${ref.token}`); - })(); } // channel getter/factory @@ -174,14 +180,14 @@ class LokiAppDotNetServerAPI { // request an token from the server async requestToken() { - const url = new URL(`${this.baseServerUrl}/loki/v1/get_challenge`); - const params = { - pubKey: this.chatAPI.ourKey, - }; - url.search = new URLSearchParams(params); - let res; try { + const url = new URL(`${this.baseServerUrl}/loki/v1/get_challenge`); + const params = { + pubKey: this.chatAPI.ourKey, + }; + url.search = new URLSearchParams(params); + res = await nodeFetch(url); } catch (e) { return null; @@ -232,15 +238,12 @@ class LokiAppDotNetServerAPI { url.search = new URLSearchParams(params); } let result; - let { token } = this; + const token = await this.getOrRefreshServerToken(); if (!token) { - token = await this.getOrRefreshServerToken(); - if (!token) { - log.error('NO TOKEN'); - return { - err: 'noToken', - }; - } + log.error('NO TOKEN'); + return { + err: 'noToken', + }; } try { const fetchOptions = {}; diff --git a/js/modules/loki_file_server_api.js b/js/modules/loki_file_server_api.js index 61caaffd3..b70984ccc 100644 --- a/js/modules/loki_file_server_api.js +++ b/js/modules/loki_file_server_api.js @@ -1,19 +1,23 @@ +/* global log */ + const LokiAppDotNetAPI = require('./loki_app_dot_net_api'); /* global log */ const DEVICE_MAPPING_ANNOTATION_KEY = 'network.loki.messenger.devicemapping'; -// returns the LokiFileServerAPI constructor with the serverUrl already consumed -function LokiFileServerAPIWrapper(serverUrl) { - return LokiFileServerAPI.bind(null, serverUrl); -} - class LokiFileServerAPI { - constructor(serverUrl, ourKey) { + constructor(ourKey) { this.ourKey = ourKey; this._adnApi = new LokiAppDotNetAPI(ourKey); - this._server = this._adnApi.findOrCreateServer(serverUrl); + } + + async establishConnection(serverUrl) { + this._server = await this._adnApi.findOrCreateServer(serverUrl); + // TODO: Handle this failure gracefully + if (!this._server) { + log.error('Failed to establish connection to file server'); + } } async getUserDeviceMapping(pubKey) { @@ -59,4 +63,4 @@ class LokiFileServerAPI { } } -module.exports = LokiFileServerAPIWrapper; +module.exports = LokiFileServerAPI; diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index ba794adca..2c52ea4a3 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -88,6 +88,11 @@ class LokiMessageAPI { }; if (isPublic) { + if (!publicSendData) { + throw new window.textsecure.PublicChatError( + 'Missing public send data for public chat message' + ); + } const res = await publicSendData.sendMessage( data.body, data.quote, diff --git a/js/views/add_server_dialog_view.js b/js/views/add_server_dialog_view.js new file mode 100644 index 000000000..270912825 --- /dev/null +++ b/js/views/add_server_dialog_view.js @@ -0,0 +1,88 @@ +/* global Whisper, i18n, _ */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + + Whisper.AddServerDialogView = Whisper.View.extend({ + templateName: 'add-server-template', + className: 'loki-dialog add-server modal', + initialize(options = {}) { + this.title = i18n('addServerDialogTitle'); + this.okText = options.okText || i18n('ok'); + this.cancelText = options.cancelText || i18n('cancel'); + this.$('input').focus(); + this.render(); + }, + events: { + keyup: 'onKeyup', + 'click .ok': 'confirm', + 'click .cancel': 'close', + }, + render_attributes() { + return { + title: this.title, + ok: this.okText, + cancel: this.cancelText, + }; + }, + confirm() { + // Remove error if there is one + this.showError(null); + const serverUrl = this.$('#server-url').val().toLowerCase(); + // TODO: Make this not hard coded + const channelId = 1; + const dialog = new Whisper.ConnectingToServerDialogView({ + serverUrl, + channelId, + }); + const dialogDelayTimer = setTimeout(() => { + this.el.append(dialog.el); + }, 200); + dialog.once('connectionResult', result => { + clearTimeout(dialogDelayTimer); + if (result.cancelled) { + this.showError(null); + return; + } + if (result.errorCode) { + this.showError(result.errorCode); + return; + } + window.Whisper.events.trigger('showToast', { + message: i18n('connectToServerSuccess'), + }); + this.close(); + }); + dialog.trigger('attemptConnection'); + }, + close() { + this.remove(); + }, + showError(message) { + if (_.isEmpty(message)) { + this.$('.error').text(''); + this.$('.error').hide(); + } else { + this.$('.error').text(`Error: ${message}`); + this.$('.error').show(); + } + this.$('input').focus(); + }, + onKeyup(event) { + switch (event.key) { + case 'Enter': + this.confirm(); + break; + case 'Escape': + case 'Esc': + this.close(); + break; + default: + break; + } + }, + }); +})(); diff --git a/js/views/app_view.js b/js/views/app_view.js index 31235bf9f..885e61ced 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -200,5 +200,9 @@ const dialog = new Whisper.QRDialogView({ string }); this.el.append(dialog.el); }, + showAddServerDialog() { + const dialog = new Whisper.AddServerDialogView(); + this.el.append(dialog.el); + }, }); })(); diff --git a/js/views/connecting_to_server_dialog_view.js b/js/views/connecting_to_server_dialog_view.js new file mode 100644 index 000000000..5c45a1ef1 --- /dev/null +++ b/js/views/connecting_to_server_dialog_view.js @@ -0,0 +1,83 @@ +/* global Whisper, i18n, lokiPublicChatAPI, ConversationController, friends */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + + Whisper.ConnectingToServerDialogView = Whisper.View.extend({ + templateName: 'connecting-to-server-template', + className: 'loki-dialog connecting-to-server modal', + initialize(options = {}) { + this.title = i18n('connectingLoad'); + this.cancelText = options.cancelText || i18n('cancel'); + this.serverUrl = options.serverUrl; + this.channelId = options.channelId; + this.once('attemptConnection', () => + this.attemptConnection(options.serverUrl, options.channelId) + ); + this.render(); + }, + events: { + keyup: 'onKeyup', + 'click .cancel': 'close', + }, + async attemptConnection(serverUrl, channelId) { + const rawServerUrl = serverUrl + .replace(/^https?:\/\//i, '') + .replace(/[/\\]+$/i, ''); + const sslServerUrl = `https://${rawServerUrl}`; + const conversationId = `publicChat:${channelId}@${rawServerUrl}`; + + const conversationExists = ConversationController.get(conversationId); + if (conversationExists) { + // We are already a member of this public chat + return this.resolveWith({ errorCode: i18n('publicChatExists') }); + } + + const serverAPI = await lokiPublicChatAPI.findOrCreateServer( + sslServerUrl + ); + if (!serverAPI) { + // Url incorrect or server not compatible + return this.resolveWith({ errorCode: i18n('connectToServerFail') }); + } + + const conversation = await ConversationController.getOrCreateAndWait( + conversationId, + 'group' + ); + serverAPI.findOrCreateChannel(channelId, conversationId); + await conversation.setPublicSource(sslServerUrl, channelId); + await conversation.setFriendRequestStatus( + friends.friendRequestStatusEnum.friends + ); + return this.resolveWith({ conversation }); + }, + resolveWith(result) { + this.trigger('connectionResult', result); + this.remove(); + }, + render_attributes() { + return { + title: this.title, + cancel: this.cancelText, + }; + }, + close() { + this.trigger('connectionResult', { cancelled: true }); + this.remove(); + }, + onKeyup(event) { + switch (event.key) { + case 'Escape': + case 'Esc': + this.close(); + break; + default: + break; + } + }, + }); +})(); diff --git a/preload.js b/preload.js index 20df787e8..9df58dadd 100644 --- a/preload.js +++ b/preload.js @@ -41,6 +41,7 @@ window.isBehindProxy = () => Boolean(config.proxyUrl); window.JobQueue = JobQueue; window.getStoragePubKey = key => window.isDev() ? key.substring(0, key.length - 2) : key; +window.getDefaultFileServer = () => config.defaultFileServer; window.isBeforeVersion = (toCheck, baseVersion) => { try { @@ -328,10 +329,7 @@ window.LokiMessageAPI = require('./js/modules/loki_message_api'); window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api'); -const LokiFileServerAPIWrapper = require('./js/modules/loki_file_server_api'); - -// bind first argument as we have it here already -window.LokiFileServerAPI = LokiFileServerAPIWrapper(config.defaultFileServer); +window.LokiFileServerAPI = require('./js/modules/loki_file_server_api'); window.LokiRssAPI = require('./js/modules/loki_rss_api'); diff --git a/test/index.html b/test/index.html index ef36a342e..4528bef6b 100644 --- a/test/index.html +++ b/test/index.html @@ -567,7 +567,8 @@ - + + diff --git a/ts/components/MainHeader.tsx b/ts/components/MainHeader.tsx index 8f099dfd3..21396b616 100644 --- a/ts/components/MainHeader.tsx +++ b/ts/components/MainHeader.tsx @@ -334,6 +334,13 @@ export class MainHeader extends React.Component { trigger('showQRDialog'); }, }, + { + id: 'showAddServer', + name: i18n('showAddServer'), + onClick: () => { + trigger('showAddServerDialog'); + }, + }, ]; const passItem = (type: string) => ({