diff --git a/src/Preferences.js b/src/Preferences.js index d6d36f6..d365230 100644 --- a/src/Preferences.js +++ b/src/Preferences.js @@ -25,6 +25,7 @@ export class Preferences extends EventEmitter { // used to differentiate web from native if a client supports both this.platform = null; this.homeservers = null; + this.customWebInstances = {}; const prefsStr = localStorage.getItem("preferred_client"); if (prefsStr) { @@ -36,6 +37,10 @@ export class Preferences extends EventEmitter { if (serversStr) { this.homeservers = JSON.parse(serversStr); } + const customWebInstancesStr = localStorage.getItem("custom_web_instances"); + if (customWebInstancesStr) { + this.customWebInstances = JSON.parse(customWebInstancesStr); + } } setClient(id, platform) { @@ -54,15 +59,27 @@ export class Preferences extends EventEmitter { } } + setCustomWebInstance(client_id, instance_url) { + this.customWebInstances[client_id] = instance_url; + this._localStorage.setItem("custom_web_instances", JSON.stringify(this.customWebInstances)); + this.emit("canClear"); + } + + getCustomWebInstance(client_id) { + return this.customWebInstances[client_id]; + } + clear() { this._localStorage.removeItem("preferred_client"); this._localStorage.removeItem("consented_servers"); + this._localStorage.removeItem("custom_web_instances"); this.clientId = null; this.platform = null; this.homeservers = null; + this.customWebInstances = {}; } get canClear() { - return !!this.clientId || !!this.platform || !!this.homeservers; + return !!this.clientId || !!this.platform || !!this.homeservers || !!this.customWebInstances; } } diff --git a/src/open/ClientView.js b/src/open/ClientView.js index 76c73e9..c73b682 100644 --- a/src/open/ClientView.js +++ b/src/open/ClientView.js @@ -39,6 +39,14 @@ function renderInstructions(parts) { export class ClientView extends TemplateView { render(t, vm) { + return t.mapView(vm => vm.customWebInstanceFormOpen, open => { + switch (open) { + case true: return new SetCustomWebInstanceView(vm); + case false: return new TemplateView(vm, t => this.renderContent(t, vm)); + } + }); + } + renderContent(t, vm) { return t.div({className: {"ClientView": true, "isPreferred": vm => vm.hasPreferredWebInstance}}, [ ... vm.hasPreferredWebInstance ? [t.div({className: "hostedBanner"}, vm.hostedByBannerLabel)] : [], t.div({className: "header"}, [ @@ -112,10 +120,49 @@ class InstallClientView extends TemplateView { } } +export class SetCustomWebInstanceView extends TemplateView { + render(t, vm) { + return t.div({className: "SetCustomWebInstanceView"}, [ + t.p([ + "Use a custom web instance for the ", t.strong(vm.name), " client:", + ]), + t.form({action: "#", id: "setCustomWebInstanceForm", onSubmit: evt => this._onSubmit(evt)}, [ + t.input({ + type: "text", + className: "fullwidth large", + placeholder: "chat.example.org", + name: "instanceHostname", + value: vm.preferredWebInstance || "", + }), + t.input({type: "submit", value: "Save", className: "primary fullwidth"}), + t.input({type: "button", value: "Use Default Instance", className: "secondary fullwidth", onClick: evt => this._onReset(evt)}), + ]) + ]); + } + + _onSubmit(evt) { + evt.preventDefault(); + const form = evt.target; + const {instanceHostname} = form.elements; + this.value.setCustomWebInstance(instanceHostname.value); + this.value.closeCustomWebInstanceForm(); + } + + _onReset(evt) { + this.value.setCustomWebInstance(undefined); + this.value.closeCustomWebInstanceForm(); + } +} + function showBack(t, vm) { return t.p({className: {caption: true, "back": true, hidden: vm => !vm.showBack}}, [ `Continue with ${vm.name} · `, t.button({className: "text", onClick: () => vm.back()}, "Change"), + t.span({hidden: vm => !vm.supportsCustomWebInstances}, [ + ' · ', + t.button({className: "text", onClick: () => vm.configureCustomWebInstance()}, "Use Custom Web Instance"), + ]) + ]); } diff --git a/src/open/ClientViewModel.js b/src/open/ClientViewModel.js index 2cd7244..210e30b 100644 --- a/src/open/ClientViewModel.js +++ b/src/open/ClientViewModel.js @@ -35,6 +35,7 @@ export class ClientViewModel extends ViewModel { this._pickClient = pickClient; // to provide "choose other client" button after calling pick() this._clientListViewModel = null; + this.customWebInstanceFormOpen = false; this._update(); } @@ -59,11 +60,11 @@ export class ClientViewModel extends ViewModel { if (this._proposedPlatform === this._nativePlatform) { deepLinkLabel = "Open in app"; } else { - deepLinkLabel = `Open on ${this._client.getPreferredWebInstance(this._link)}`; + deepLinkLabel = `Open on ${this.preferredWebInstance}`; } } const actions = []; - const proposedDeepLink = this._client.getDeepLink(this._proposedPlatform, this._link); + const proposedDeepLink = this._client.getDeepLink(this._proposedPlatform, this._link, this.preferredWebInstance); if (proposedDeepLink) { actions.push({ label: deepLinkLabel, @@ -83,8 +84,8 @@ export class ClientViewModel extends ViewModel { // show only if there is a preferred instance, and if we don't already link to it in the first button if (hasPreferredWebInstance && this._webPlatform && this._proposedPlatform !== this._webPlatform) { actions.push({ - label: `Open on ${this._client.getPreferredWebInstance(this._link)}`, - url: this._client.getDeepLink(this._webPlatform, this._link), + label: `Open on ${this.preferredWebInstance}`, + url: this._client.getDeepLink(this._webPlatform, this._link, this.preferredWebInstance), kind: "open-in-web", activated: () => {} // don't persist this choice as we don't persist the preferred web instance, it's in the url }); @@ -108,10 +109,10 @@ export class ClientViewModel extends ViewModel { actions.push(...nativeActions); } if (this._webPlatform) { - const webDeepLink = this._client.getDeepLink(this._webPlatform, this._link); + const webDeepLink = this._client.getDeepLink(this._webPlatform, this._link, this.preferredWebInstance); if (webDeepLink) { const webLabel = this.hasPreferredWebInstance ? - `Open on ${this._client.getPreferredWebInstance(this._link)}` : + `Open on ${this.preferredWebInstance}` : `Continue in your browser`; actions.push({ label: webLabel, @@ -128,18 +129,26 @@ export class ClientViewModel extends ViewModel { return actions; } - get hasPreferredWebInstance() { + get preferredWebInstance() { // also check there is a web platform that matches the platforms the user is on (mobile or desktop web) - return this._webPlatform && typeof this._client.getPreferredWebInstance(this._link) === "string"; + if (!this._webPlatform) return undefined; + return ( + this.preferences.getCustomWebInstance(this._client.id) + || this._client.getPreferredWebInstance(this._link) + ); + } + + get hasPreferredWebInstance() { + return typeof this.preferredWebInstance === "string"; } get hostedByBannerLabel() { - const preferredWebInstance = this._client.getPreferredWebInstance(this._link); - if (this._webPlatform && preferredWebInstance) { + if (this.hasPreferredWebInstance) { + const preferredWebInstance = this.preferredWebInstance; let label = preferredWebInstance; - const subDomainIdx = preferredWebInstance.lastIndexOf(".", preferredWebInstance.lastIndexOf(".")); + const subDomainIdx = preferredWebInstance.lastIndexOf(".", preferredWebInstance.lastIndexOf(".") - 1); if (subDomainIdx !== -1) { - label = preferredWebInstance.slice(preferredWebInstance.length - subDomainIdx + 1); + label = preferredWebInstance.slice(subDomainIdx + 1); } return `Hosted by ${label}`; } @@ -188,7 +197,7 @@ export class ClientViewModel extends ViewModel { get showDeepLinkInInstall() { // we can assume this._nativePlatform as this._clientCanIntercept already checks it - return this._clientCanIntercept && !!this._client.getDeepLink(this._nativePlatform, this._link); + return this._clientCanIntercept && !!this._client.getDeepLink(this._nativePlatform, this._link, this.preferredWebInstance); } get availableOnPlatformNames() { @@ -223,6 +232,10 @@ export class ClientViewModel extends ViewModel { return !!this._clientListViewModel; } + get supportsCustomWebInstances() { + return !!this._client.supportsCustomInstances; + } + back() { if (this._clientListViewModel) { const vm = this._clientListViewModel; @@ -231,9 +244,29 @@ export class ClientViewModel extends ViewModel { // in the list with all clients, and also if we refresh, we get the list with // all clients rather than having our "change client" click reverted. this.preferences.setClient(undefined, undefined); + this.preferences.setCustomWebInstance(this._client.id, undefined); this._update(); this.emitChange(); vm.showAll(); } } + + configureCustomWebInstance() { + this.customWebInstanceFormOpen = true; + this.emitChange(); + } + + closeCustomWebInstanceForm() { + this.customWebInstanceFormOpen = false; + this.emitChange(); + } + + setCustomWebInstance(hostname) { + if (hostname) { + hostname = hostname.trim().replace(/^https:\/\//, '').replace(/\/.*$/, ''); + } + this.preferences.setClient(this._client.id, hostname ? this._webPlatform : (this._nativePlatform || this._webPlatform)); + this.preferences.setCustomWebInstance(this._client.id, hostname || undefined); + this._update(); + } } diff --git a/src/open/clients/Element.js b/src/open/clients/Element.js index 06ca2fe..e28b0ac 100644 --- a/src/open/clients/Element.js +++ b/src/open/clients/Element.js @@ -55,8 +55,9 @@ export class Element { get homepage() { return "https://element.io"; } get author() { return "Element"; } getMaturity(platform) { return Maturity.Stable; } + get supportsCustomInstances() { return true; } - getDeepLink(platform, link) { + getDeepLink(platform, link, preferredWebInstance) { let fragmentPath; switch (link.kind) { case LinkKind.User: @@ -82,8 +83,8 @@ export class Element { let instanceHost = trustedWebInstances[0]; // we use app.element.io which iOS will intercept, but it likely won't intercept any other trusted instances // so only use a preferred web instance for true web links. - if (isWebPlatform && trustedWebInstances.includes(link.webInstances[this.id])) { - instanceHost = link.webInstances[this.id]; + if (isWebPlatform && preferredWebInstance) { + instanceHost = preferredWebInstance; } return `https://${instanceHost}/#/${fragmentPath}`; } else if (platform === Platform.Linux || platform === Platform.Windows || platform === Platform.macOS) {