From 22ad0e928926797255aa9bc358a718ad490d4a97 Mon Sep 17 00:00:00 2001 From: Mira Nord Date: Sun, 16 Mar 2025 19:16:06 +0100 Subject: [PATCH 1/5] Improve domain splitting for "Hosted by" header --- src/open/ClientViewModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/open/ClientViewModel.js b/src/open/ClientViewModel.js index 2cd72449..9a0543a1 100644 --- a/src/open/ClientViewModel.js +++ b/src/open/ClientViewModel.js @@ -137,9 +137,9 @@ export class ClientViewModel extends ViewModel { const preferredWebInstance = this._client.getPreferredWebInstance(this._link); if (this._webPlatform && 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}`; } From 50a25dd04c6b99ffcc012186bffbb9b4f3464b79 Mon Sep 17 00:00:00 2001 From: Mira Nord Date: Sun, 16 Mar 2025 20:54:38 +0100 Subject: [PATCH 2/5] Add preference for custom web instances, use it for Element --- src/Preferences.js | 17 +++++++++++++++++ src/open/ClientViewModel.js | 30 +++++++++++++++++++----------- src/open/clients/Element.js | 7 ++++--- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/Preferences.js b/src/Preferences.js index d6d36f69..cdd14462 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.preferredWebInstances = {}; 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 preferredWebInstancesStr = localStorage.getItem("preferred_web_instances"); + if (preferredWebInstancesStr) { + this.preferredWebInstances = JSON.parse(preferredWebInstancesStr); + } } setClient(id, platform) { @@ -54,12 +59,24 @@ export class Preferences extends EventEmitter { } } + setPreferredWebInstance(client_id, instance_url) { + this.preferredWebInstances[client_id] = instance_url; + this._localStorage.setItem("preferred_web_instances", JSON.stringify(this.preferredWebInstances)); + this.emit("canClear"); + } + + getPreferredWebInstance(client_id) { + return this.preferredWebInstances[client_id]; + } + clear() { this._localStorage.removeItem("preferred_client"); this._localStorage.removeItem("consented_servers"); + this._localStorage.removeItem("preferred_web_instances"); this.clientId = null; this.platform = null; this.homeservers = null; + this.preferredWebInstances = {}; } get canClear() { diff --git a/src/open/ClientViewModel.js b/src/open/ClientViewModel.js index 9a0543a1..70591865 100644 --- a/src/open/ClientViewModel.js +++ b/src/open/ClientViewModel.js @@ -59,11 +59,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 +83,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 +108,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,14 +128,22 @@ 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.getPreferredWebInstance(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(".") - 1); if (subDomainIdx !== -1) { @@ -188,7 +196,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() { diff --git a/src/open/clients/Element.js b/src/open/clients/Element.js index 06ca2fef..e28b0acb 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) { From 06237b1b8b2405a8454ef79af1e6430d238c4535 Mon Sep 17 00:00:00 2001 From: Mira Nord Date: Sun, 16 Mar 2025 20:55:20 +0100 Subject: [PATCH 3/5] Add link to change custom web instance --- src/open/ClientView.js | 31 +++++++++++++++++++++++++++++++ src/open/ClientViewModel.js | 5 +++++ 2 files changed, 36 insertions(+) diff --git a/src/open/ClientView.js b/src/open/ClientView.js index 76c73e9f..b42eb16c 100644 --- a/src/open/ClientView.js +++ b/src/open/ClientView.js @@ -112,10 +112,41 @@ 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.label([ + "Host name:", + t.input({ + type: "text", + className: "line", + placeholder: "chat.example.org", + name: "instanceHostname", + }) + ]) + ]) + ]); + } + + _onSubmit(evt) { + evt.preventDefault(); + this.value.continueWithSelection(this._askEveryTimeChecked); + } +} + 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.showSetWebInstance}, [ + ' · ', + t.button({className: "text", onClick: () => vm.setCustomWebInstance()}, "Use Custom Web Instance"), + ]) + ]); } diff --git a/src/open/ClientViewModel.js b/src/open/ClientViewModel.js index 70591865..380512f6 100644 --- a/src/open/ClientViewModel.js +++ b/src/open/ClientViewModel.js @@ -231,6 +231,10 @@ export class ClientViewModel extends ViewModel { return !!this._clientListViewModel; } + get showSetWebInstance() { + return !!this._client.supportsCustomInstances; + } + back() { if (this._clientListViewModel) { const vm = this._clientListViewModel; @@ -239,6 +243,7 @@ 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.setPreferredWebInstance(this._client.id, undefined); this._update(); this.emitChange(); vm.showAll(); From d993157cfae9dced77e8f5ac772b5b14bc46a6aa Mon Sep 17 00:00:00 2001 From: Mira Nord Date: Sun, 16 Mar 2025 21:20:06 +0100 Subject: [PATCH 4/5] Add form to configure custom web instance --- src/Preferences.js | 24 +++++++++++----------- src/open/ClientView.js | 40 ++++++++++++++++++++++++++----------- src/open/ClientViewModel.js | 23 ++++++++++++++++++--- 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/src/Preferences.js b/src/Preferences.js index cdd14462..d365230e 100644 --- a/src/Preferences.js +++ b/src/Preferences.js @@ -25,7 +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.preferredWebInstances = {}; + this.customWebInstances = {}; const prefsStr = localStorage.getItem("preferred_client"); if (prefsStr) { @@ -37,9 +37,9 @@ export class Preferences extends EventEmitter { if (serversStr) { this.homeservers = JSON.parse(serversStr); } - const preferredWebInstancesStr = localStorage.getItem("preferred_web_instances"); - if (preferredWebInstancesStr) { - this.preferredWebInstances = JSON.parse(preferredWebInstancesStr); + const customWebInstancesStr = localStorage.getItem("custom_web_instances"); + if (customWebInstancesStr) { + this.customWebInstances = JSON.parse(customWebInstancesStr); } } @@ -59,27 +59,27 @@ export class Preferences extends EventEmitter { } } - setPreferredWebInstance(client_id, instance_url) { - this.preferredWebInstances[client_id] = instance_url; - this._localStorage.setItem("preferred_web_instances", JSON.stringify(this.preferredWebInstances)); + setCustomWebInstance(client_id, instance_url) { + this.customWebInstances[client_id] = instance_url; + this._localStorage.setItem("custom_web_instances", JSON.stringify(this.customWebInstances)); this.emit("canClear"); } - getPreferredWebInstance(client_id) { - return this.preferredWebInstances[client_id]; + getCustomWebInstance(client_id) { + return this.customWebInstances[client_id]; } clear() { this._localStorage.removeItem("preferred_client"); this._localStorage.removeItem("consented_servers"); - this._localStorage.removeItem("preferred_web_instances"); + this._localStorage.removeItem("custom_web_instances"); this.clientId = null; this.platform = null; this.homeservers = null; - this.preferredWebInstances = {}; + 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 b42eb16c..37c2b9e7 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"}, [ @@ -119,22 +127,30 @@ export class SetCustomWebInstanceView extends TemplateView { "Use a custom web instance for the ", t.strong(vm.name), " client:", ]), t.form({action: "#", id: "setCustomWebInstanceForm", onSubmit: evt => this._onSubmit(evt)}, [ - t.label([ - "Host name:", - t.input({ - type: "text", - className: "line", - placeholder: "chat.example.org", - name: "instanceHostname", - }) - ]) + 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(); - this.value.continueWithSelection(this._askEveryTimeChecked); + const form = evt.target; + const {instanceHostname} = form.elements; + this.value.setCustomWebInstance(instanceHostname.value || undefined); + this.value.closeCustomWebInstanceForm(); + } + + _onReset(evt) { + this.value.setCustomWebInstance(undefined); + this.value.closeCustomWebInstanceForm(); } } @@ -142,9 +158,9 @@ 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.showSetWebInstance}, [ + t.span({hidden: vm => !vm.supportsCustomWebInstances}, [ ' · ', - t.button({className: "text", onClick: () => vm.setCustomWebInstance()}, "Use Custom Web Instance"), + t.button({className: "text", onClick: () => vm.configureCustomWebInstance()}, "Use Custom Web Instance"), ]) ]); diff --git a/src/open/ClientViewModel.js b/src/open/ClientViewModel.js index 380512f6..b2e5f87c 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(); } @@ -132,7 +133,7 @@ export class ClientViewModel extends ViewModel { // also check there is a web platform that matches the platforms the user is on (mobile or desktop web) if (!this._webPlatform) return undefined; return ( - this.preferences.getPreferredWebInstance(this._client.id) + this.preferences.getCustomWebInstance(this._client.id) || this._client.getPreferredWebInstance(this._link) ); } @@ -231,7 +232,7 @@ export class ClientViewModel extends ViewModel { return !!this._clientListViewModel; } - get showSetWebInstance() { + get supportsCustomWebInstances() { return !!this._client.supportsCustomInstances; } @@ -243,10 +244,26 @@ 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.setPreferredWebInstance(this._client.id, 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) { + this.preferences.setClient(this._client.id, hostname ? this._webPlatform : (this._nativePlatform || this._webPlatform)); + this.preferences.setCustomWebInstance(this._client.id, hostname); + this._update(); + } } From 6dd9a0213cf77623900102334d5ed30b20068cbf Mon Sep 17 00:00:00 2001 From: Mira Nord Date: Sun, 16 Mar 2025 21:40:52 +0100 Subject: [PATCH 5/5] Trim whitespace and protocol / path information --- src/open/ClientView.js | 2 +- src/open/ClientViewModel.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/open/ClientView.js b/src/open/ClientView.js index 37c2b9e7..c73b682d 100644 --- a/src/open/ClientView.js +++ b/src/open/ClientView.js @@ -144,7 +144,7 @@ export class SetCustomWebInstanceView extends TemplateView { evt.preventDefault(); const form = evt.target; const {instanceHostname} = form.elements; - this.value.setCustomWebInstance(instanceHostname.value || undefined); + this.value.setCustomWebInstance(instanceHostname.value); this.value.closeCustomWebInstanceForm(); } diff --git a/src/open/ClientViewModel.js b/src/open/ClientViewModel.js index b2e5f87c..210e30bc 100644 --- a/src/open/ClientViewModel.js +++ b/src/open/ClientViewModel.js @@ -262,8 +262,11 @@ export class ClientViewModel extends ViewModel { } 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); + this.preferences.setCustomWebInstance(this._client.id, hostname || undefined); this._update(); } }