From bfcac9fafb3c145b60fdf06378d879cd2349524b Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Mon, 23 Aug 2021 14:38:07 -0700 Subject: [PATCH 01/18] Try open matrix: links before showing clients --- src/Link.js | 21 +++++++++++++++++++++ src/RootView.js | 28 +++++++++++++++------------- src/RootViewModel.js | 10 ++++++++++ 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/Link.js b/src/Link.js index 079d7be4..a37cfc80 100644 --- a/src/Link.js +++ b/src/Link.js @@ -40,6 +40,16 @@ function asPrefix(identifierKind) { } } +function asPath(identifierKind) { + switch (identifierKind) { + case IdentifierKind.RoomId: return "roomid"; + case IdentifierKind.RoomAlias: return "r"; + case IdentifierKind.GroupId: return null; + case IdentifierKind.UserId: return "u"; + default: throw new Error("invalid id kind " + identifierKind); + } +} + function getWebInstanceMap(queryParams) { const prefix = "web-instance["; const postfix = "]"; @@ -183,4 +193,15 @@ export class Link { return `/${this.identifier}`; } } + + toMatrixUrl() { + const prefix = asPath(this.identifierKind); + if (!prefix) { + // Some matrix.to links aren't valid matrix: links (i.e. groups) + return null; + } + const identifier = this.identifier.substring(1); + const suffix = this.eventId ? `/e/${this.eventId.substring(1)}` : ""; + return `matrix:${prefix}/${identifier}${suffix}`; + } } diff --git a/src/RootView.js b/src/RootView.js index 315fbd96..c076ac74 100644 --- a/src/RootView.js +++ b/src/RootView.js @@ -21,21 +21,23 @@ import {LoadServerPolicyView} from "./policy/LoadServerPolicyView.js"; export class RootView extends TemplateView { render(t, vm) { - return t.div({className: "RootView"}, [ - t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null), - t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null), - t.mapView(vm => vm.loadServerPolicyViewModel, vm => vm ? new LoadServerPolicyView(vm) : null), - t.div({className: "footer"}, [ - t.p(t.img({src: "images/matrix-logo.svg"})), - t.p(["This invite uses ", externalLink(t, "https://matrix.org", "Matrix"), ", an open network for secure, decentralized communication."]), - t.ul({className: "links"}, [ - t.li(externalLink(t, "https://github.com/matrix-org/matrix.to", "GitHub project")), - t.li(externalLink(t, "https://github.com/matrix-org/matrix.to/tree/main/src/open/clients", "Add your app")), - t.li({className: {hidden: vm => !vm.hasPreferences}}, - t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")), + return t.if(vm => !vm.hidden, + vm => t.div({className: "RootView"}, [ + t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null), + t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null), + t.mapView(vm => vm.loadServerPolicyViewModel, vm => vm ? new LoadServerPolicyView(vm) : null), + t.div({className: "footer"}, [ + t.p(t.img({src: "images/matrix-logo.svg"})), + t.p(["This invite uses ", externalLink(t, "https://matrix.org", "Matrix"), ", an open network for secure, decentralized communication."]), + t.ul({className: "links"}, [ + t.li(externalLink(t, "https://github.com/matrix-org/matrix.to", "GitHub project")), + t.li(externalLink(t, "https://github.com/matrix-org/matrix.to/tree/main/src/open/clients", "Add your app")), + t.li({className: {hidden: vm => !vm.hasPreferences}}, + t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")), + ]) ]) ]) - ]); + ); } } diff --git a/src/RootViewModel.js b/src/RootViewModel.js index 6c05cc9c..c725c91b 100644 --- a/src/RootViewModel.js +++ b/src/RootViewModel.js @@ -25,6 +25,7 @@ import {Platform} from "./Platform.js"; export class RootViewModel extends ViewModel { constructor(options) { super(options); + this.hidden = false; this.link = null; this.openLinkViewModel = null; this.createLinkViewModel = null; @@ -58,6 +59,15 @@ export class RootViewModel extends ViewModel { } else { const oldLink = this.link; this.link = Link.parse(hash); + const matrixUrl = this.link.toMatrixUrl() + if (matrixUrl) { + this.hidden = true; + setTimeout(() => { + this.hidden = false; + this.emitChange(); + }, 1000); + this.openLink(this.link.toMatrixUrl()); + } this._updateChildVMs(oldLink); } } From b17ce2ee13dbac5fea33a60fd8f0040e2b046131 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 24 Aug 2021 12:24:09 -0700 Subject: [PATCH 02/18] Take the hiding code out of the root view model --- src/RootView.js | 28 +++++++++++++--------------- src/RootViewModel.js | 10 ---------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/RootView.js b/src/RootView.js index c076ac74..315fbd96 100644 --- a/src/RootView.js +++ b/src/RootView.js @@ -21,23 +21,21 @@ import {LoadServerPolicyView} from "./policy/LoadServerPolicyView.js"; export class RootView extends TemplateView { render(t, vm) { - return t.if(vm => !vm.hidden, - vm => t.div({className: "RootView"}, [ - t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null), - t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null), - t.mapView(vm => vm.loadServerPolicyViewModel, vm => vm ? new LoadServerPolicyView(vm) : null), - t.div({className: "footer"}, [ - t.p(t.img({src: "images/matrix-logo.svg"})), - t.p(["This invite uses ", externalLink(t, "https://matrix.org", "Matrix"), ", an open network for secure, decentralized communication."]), - t.ul({className: "links"}, [ - t.li(externalLink(t, "https://github.com/matrix-org/matrix.to", "GitHub project")), - t.li(externalLink(t, "https://github.com/matrix-org/matrix.to/tree/main/src/open/clients", "Add your app")), - t.li({className: {hidden: vm => !vm.hasPreferences}}, - t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")), - ]) + return t.div({className: "RootView"}, [ + t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null), + t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null), + t.mapView(vm => vm.loadServerPolicyViewModel, vm => vm ? new LoadServerPolicyView(vm) : null), + t.div({className: "footer"}, [ + t.p(t.img({src: "images/matrix-logo.svg"})), + t.p(["This invite uses ", externalLink(t, "https://matrix.org", "Matrix"), ", an open network for secure, decentralized communication."]), + t.ul({className: "links"}, [ + t.li(externalLink(t, "https://github.com/matrix-org/matrix.to", "GitHub project")), + t.li(externalLink(t, "https://github.com/matrix-org/matrix.to/tree/main/src/open/clients", "Add your app")), + t.li({className: {hidden: vm => !vm.hasPreferences}}, + t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")), ]) ]) - ); + ]); } } diff --git a/src/RootViewModel.js b/src/RootViewModel.js index c725c91b..6c05cc9c 100644 --- a/src/RootViewModel.js +++ b/src/RootViewModel.js @@ -25,7 +25,6 @@ import {Platform} from "./Platform.js"; export class RootViewModel extends ViewModel { constructor(options) { super(options); - this.hidden = false; this.link = null; this.openLinkViewModel = null; this.createLinkViewModel = null; @@ -59,15 +58,6 @@ export class RootViewModel extends ViewModel { } else { const oldLink = this.link; this.link = Link.parse(hash); - const matrixUrl = this.link.toMatrixUrl() - if (matrixUrl) { - this.hidden = true; - setTimeout(() => { - this.hidden = false; - this.emitChange(); - }, 1000); - this.openLink(this.link.toMatrixUrl()); - } this._updateChildVMs(oldLink); } } From b239f495804dad22c35b3be5b3a5138df576665c Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 24 Aug 2021 12:24:45 -0700 Subject: [PATCH 03/18] Track hidden status in OpenLinkViewModel --- src/open/OpenLinkViewModel.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/open/OpenLinkViewModel.js b/src/open/OpenLinkViewModel.js index 2b5d2954..2580d38c 100644 --- a/src/open/OpenLinkViewModel.js +++ b/src/open/OpenLinkViewModel.js @@ -32,6 +32,8 @@ export class OpenLinkViewModel extends ViewModel { this.previewViewModel = null; this.clientsViewModel = null; this.previewLoading = false; + this.tryingLink = false; + this._tryLink(); if (this.preferences.homeservers === null) { this._showServerConsent(); } else { @@ -39,6 +41,18 @@ export class OpenLinkViewModel extends ViewModel { } } + _tryLink() { + const matrixUrl = this._link.toMatrixUrl() + if (matrixUrl) { + this.tryingLink = true; + setTimeout(() => { + this.tryingLink = false; + this.emitChange(); + }, 5000); + this.openLink(matrixUrl); + } + } + _showServerConsent() { let servers = []; if (this.preferences.homeservers) { From c26b8dcec12c0a8c65ff646384bf81374471a7c8 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 24 Aug 2021 13:12:40 -0700 Subject: [PATCH 04/18] Display a little message while trying to redirect --- css/open.css | 20 ++++++++++++++++++++ src/open/OpenLinkView.js | 20 +++++++++++++++++--- src/open/OpenLinkViewModel.js | 2 +- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/css/open.css b/css/open.css index 560f7a32..25f3930c 100644 --- a/css/open.css +++ b/css/open.css @@ -50,3 +50,23 @@ limitations under the License. border-bottom: 1px solid var(--grey); padding: 4px 0; } + +.OpeningClientView { + display: flex; + align-items: center; + flex-direction: column; + margin: 0; +} + +.OpeningClientView .defaultAvatar { + width: 64px; + height: 64px; + background-image: url('../images/chat-icon.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: 85%; +} + +.OpeningClientView .spinner { + margin-top: 15px; +} diff --git a/src/open/OpenLinkView.js b/src/open/OpenLinkView.js index c7dfe0fa..d5458856 100644 --- a/src/open/OpenLinkView.js +++ b/src/open/OpenLinkView.js @@ -22,14 +22,28 @@ import {ServerConsentView} from "./ServerConsentView.js"; export class OpenLinkView extends TemplateView { render(t, vm) { return t.div({className: "OpenLinkView card"}, [ - t.mapView(vm => vm.previewViewModel, previewVM => previewVM ? - new ShowLinkView(vm) : - new ServerConsentView(vm.serverConsentViewModel) + t.map(vm => vm.tryingLink, tryingLink => tryingLink ? + t.view(new TryingLinkView(vm)) : + t.mapView(vm => vm.previewViewModel, previewVM => previewVM ? + new ShowLinkView(vm) : + new ServerConsentView(vm.serverConsentViewModel) + ), ), ]); } } +class TryingLinkView extends TemplateView { + render (t, vm) { + return t.div({ className: "OpeningClientView" }, [ + t.div({className: "defaultAvatar"}), + t.h1("Trying to open your default client..."), + t.span("If this doesn't work, you will be redirected shortly."), + t.div({className: "spinner"}), + ]); + } +} + class ShowLinkView extends TemplateView { render(t, vm) { return t.div([ diff --git a/src/open/OpenLinkViewModel.js b/src/open/OpenLinkViewModel.js index 2580d38c..ee5d17c2 100644 --- a/src/open/OpenLinkViewModel.js +++ b/src/open/OpenLinkViewModel.js @@ -48,7 +48,7 @@ export class OpenLinkViewModel extends ViewModel { setTimeout(() => { this.tryingLink = false; this.emitChange(); - }, 5000); + }, 1000); this.openLink(matrixUrl); } } From b8e07cda7ea0083aced4a409f31af4eeaa1e4763 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 24 Aug 2021 13:24:53 -0700 Subject: [PATCH 05/18] Receive setTimeout as argument to help mocking --- src/main.js | 1 + src/open/OpenLinkViewModel.js | 2 +- src/utils/ViewModel.js | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 9826dfd5..eb01795b 100644 --- a/src/main.js +++ b/src/main.js @@ -24,6 +24,7 @@ export async function main(container) { const vm = new RootViewModel({ request: xhrRequest, openLink: url => location.href = url, + setTimeout: (f, time) => window.setTimeout(f, time), platforms: guessApplicablePlatforms(navigator.userAgent, navigator.platform), preferences: new Preferences(window.localStorage), origin: location.origin, diff --git a/src/open/OpenLinkViewModel.js b/src/open/OpenLinkViewModel.js index ee5d17c2..2fd41296 100644 --- a/src/open/OpenLinkViewModel.js +++ b/src/open/OpenLinkViewModel.js @@ -45,7 +45,7 @@ export class OpenLinkViewModel extends ViewModel { const matrixUrl = this._link.toMatrixUrl() if (matrixUrl) { this.tryingLink = true; - setTimeout(() => { + this.setTimeout(() => { this.tryingLink = false; this.emitChange(); }, 1000); diff --git a/src/utils/ViewModel.js b/src/utils/ViewModel.js index b9afbc1c..274dd6b1 100644 --- a/src/utils/ViewModel.js +++ b/src/utils/ViewModel.js @@ -63,6 +63,7 @@ export class ViewModel extends EventEmitter { get request() { return this._options.request; } get origin() { return this._options.origin; } get openLink() { return this._options.openLink; } + get setTimeout() { return this._options.setTimeout; } get platforms() { return this._options.platforms; } get preferences() { return this._options.preferences; } @@ -71,6 +72,7 @@ export class ViewModel extends EventEmitter { request: this.request, origin: this.origin, openLink: this.openLink, + setTimeout: this.setTimeout, platforms: this.platforms, preferences: this.preferences, }, options); From ee820b6ca1f45f317b1fa301767181a078f427f6 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 24 Aug 2021 13:32:36 -0700 Subject: [PATCH 06/18] URLencode identifiers --- src/Link.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Link.js b/src/Link.js index a37cfc80..1f72be6e 100644 --- a/src/Link.js +++ b/src/Link.js @@ -30,6 +30,10 @@ export const IdentifierKind = createEnum( "GroupId", ); +function idToPath(identifier) { + return encodeURIComponent(identifier.substring(1)); +} + function asPrefix(identifierKind) { switch (identifierKind) { case IdentifierKind.RoomId: return "!"; @@ -200,8 +204,8 @@ export class Link { // Some matrix.to links aren't valid matrix: links (i.e. groups) return null; } - const identifier = this.identifier.substring(1); - const suffix = this.eventId ? `/e/${this.eventId.substring(1)}` : ""; + const identifier = idToPath(this.identifier); + const suffix = this.eventId ? `/e/${idToPath(this.eventId)}` : ""; return `matrix:${prefix}/${identifier}${suffix}`; } } From 94f0883fec8581a78ac41aa29c5c52bdbc0c5a38 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 24 Aug 2021 15:20:48 -0700 Subject: [PATCH 07/18] Center text in addition to aligning --- css/open.css | 1 + 1 file changed, 1 insertion(+) diff --git a/css/open.css b/css/open.css index 25f3930c..326c8eca 100644 --- a/css/open.css +++ b/css/open.css @@ -54,6 +54,7 @@ limitations under the License. .OpeningClientView { display: flex; align-items: center; + text-align: center; flex-direction: column; margin: 0; } From 820a090a71f53f1d30f439082f53905d65fac797 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Wed, 25 Aug 2021 17:23:23 -0700 Subject: [PATCH 08/18] Extract platform selection code from ClientView --- src/open/ClientViewModel.js | 17 +++++------------ src/open/clients/index.js | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/open/ClientViewModel.js b/src/open/ClientViewModel.js index 39873153..84e2329f 100644 --- a/src/open/ClientViewModel.js +++ b/src/open/ClientViewModel.js @@ -17,14 +17,7 @@ limitations under the License. import {isWebPlatform, isDesktopPlatform, Platform} from "../Platform.js"; import {ViewModel} from "../utils/ViewModel.js"; import {IdentifierKind} from "../Link.js"; - -function getMatchingPlatforms(client, supportedPlatforms) { - const clientPlatforms = client.platforms; - const matchingPlatforms = supportedPlatforms.filter(p => { - return clientPlatforms.includes(p); - }); - return matchingPlatforms; -} +import {getMatchingPlatforms, selectPlatforms} from "./clients/index.js"; export class ClientViewModel extends ViewModel { constructor(options) { @@ -40,10 +33,10 @@ export class ClientViewModel extends ViewModel { _update() { const matchingPlatforms = getMatchingPlatforms(this._client, this.platforms); - this._webPlatform = matchingPlatforms.find(p => isWebPlatform(p)); - this._nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p)); - const preferredPlatform = matchingPlatforms.find(p => p === this.preferences.platform); - this._proposedPlatform = preferredPlatform || this._nativePlatform || this._webPlatform; + const {proposedPlatform, nativePlatform, webPlatform} = selectPlatforms(matchingPlatforms, this.preferences.platform); + this._nativePlatform = nativePlatform; + this._webPlatform = webPlatform; + this._proposedPlatform = proposedPlatform; this.openActions = this._createOpenActions(); this.installActions = this._createInstallActions(); diff --git a/src/open/clients/index.js b/src/open/clients/index.js index 44acee29..46a5fea4 100644 --- a/src/open/clients/index.js +++ b/src/open/clients/index.js @@ -21,6 +21,25 @@ import {Fractal} from "./Fractal.js"; import {Quaternion} from "./Quaternion.js"; import {Tensor} from "./Tensor.js"; import {Fluffychat} from "./Fluffychat.js"; +import {isWebPlatform} from "../../Platform.js" + +export function getMatchingPlatforms(client, supportedPlatforms) { + const clientPlatforms = client.platforms; + const matchingPlatforms = supportedPlatforms.filter(p => { + return clientPlatforms.includes(p); + }); + return matchingPlatforms; +} + +export function selectPlatforms(matchingPlatforms, userPreferredPlatform) { + const webPlatform = matchingPlatforms.find(p => isWebPlatform(p)); + const nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p)); + const preferredPlatform = matchingPlatforms.find(p => p === userPreferredPlatform); + return { + proposedPlatform: preferredPlatform || nativePlatform || webPlatform, + nativePlatform, webPlatform + }; +} export function createClients() { return [ From a781c527fd4910438f930b00495367b654526570 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 26 Aug 2021 14:13:45 -0700 Subject: [PATCH 09/18] Add a view model for opening a client --- src/open/OpenDefaultViewModel.js | 67 ++++++++++++++++++++++++++++++++ src/open/OpenLinkView.js | 17 ++++---- src/open/OpenLinkViewModel.js | 66 ++++++++++++++++++++++++------- 3 files changed, 128 insertions(+), 22 deletions(-) create mode 100644 src/open/OpenDefaultViewModel.js diff --git a/src/open/OpenDefaultViewModel.js b/src/open/OpenDefaultViewModel.js new file mode 100644 index 00000000..b187046c --- /dev/null +++ b/src/open/OpenDefaultViewModel.js @@ -0,0 +1,67 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "../utils/ViewModel.js"; +import {getMatchingPlatforms, selectPlatforms} from "./clients/index.js"; + +export class OpenDefaultViewModel extends ViewModel { + constructor(options) { + super(options); + const {client, link, openLinkVM, proposedPlatform, webPlatform} = options; + this._client = client; + this._link = link; + this._openLinkVM = openLinkVM; + this._proposedPlatform = proposedPlatform; + this._webPlatform = webPlatform; + } + + get name() { + return this._client?.name; + } + + get openingDefault() { + return !this._client; + } + + get autoRedirect() { + // Only auto-redirect when a preferred client hasn't been set. + return this.openingDefault; + } + + get webDeepLink() { + return this._client && this._webPlatform && this._client.getDeepLink(this._webPlatform); + } + + close() { + this._openLinkVM.closeDefault(); + } + + tryOpenLink() { + this._trying = true; + // TODO actually try opening link + this.setTimeout(() => { + if (this.autoRedirect) { + // We're about to be closed so don't + // bother with visual updates. + this.close(); + } else { + this._trying = false; + this.emitChange(); + } + }, 1000); + this.emitChange(); + } +} diff --git a/src/open/OpenLinkView.js b/src/open/OpenLinkView.js index d5458856..b859b063 100644 --- a/src/open/OpenLinkView.js +++ b/src/open/OpenLinkView.js @@ -22,17 +22,20 @@ import {ServerConsentView} from "./ServerConsentView.js"; export class OpenLinkView extends TemplateView { render(t, vm) { return t.div({className: "OpenLinkView card"}, [ - t.map(vm => vm.tryingLink, tryingLink => tryingLink ? - t.view(new TryingLinkView(vm)) : - t.mapView(vm => vm.previewViewModel, previewVM => previewVM ? - new ShowLinkView(vm) : - new ServerConsentView(vm.serverConsentViewModel) - ), - ), + t.mapView(vm => [ vm.openDefaultViewModel, vm.previewViewModel ], ([openDefaultVM, previewVM]) => { + if (openDefaultVM) { + return new TryingLinkView(openDefaultVM) + } else if (previewVM) { + return new ShowLinkView(vm); + } else { + return new ServerConsentView(vm.serverConsentViewModel); + } + }), ]); } } + class TryingLinkView extends TemplateView { render (t, vm) { return t.div({ className: "OpeningClientView" }, [ diff --git a/src/open/OpenLinkViewModel.js b/src/open/OpenLinkViewModel.js index 2fd41296..97fdac46 100644 --- a/src/open/OpenLinkViewModel.js +++ b/src/open/OpenLinkViewModel.js @@ -18,9 +18,11 @@ import {ViewModel} from "../utils/ViewModel.js"; import {ClientListViewModel} from "./ClientListViewModel.js"; import {ClientViewModel} from "./ClientViewModel.js"; import {PreviewViewModel} from "../preview/PreviewViewModel.js"; +import {OpenDefaultViewModel} from "./OpenDefaultViewModel.js"; import {ServerConsentViewModel} from "./ServerConsentViewModel.js"; import {getLabelForLinkKind} from "../Link.js"; import {orderedUnique} from "../utils/unique.js"; +import {getMatchingPlatforms, selectPlatforms} from "./clients/index.js"; export class OpenLinkViewModel extends ViewModel { constructor(options) { @@ -28,12 +30,53 @@ export class OpenLinkViewModel extends ViewModel { const {clients, link} = options; this._link = link; this._clients = clients; + this.openDefaultViewModel = null; this.serverConsentViewModel = null; this.previewViewModel = null; this.clientsViewModel = null; this.previewLoading = false; this.tryingLink = false; - this._tryLink(); + if (!this._tryOpenDefault()) { + this._activeOpen(); + } + } + + _tryOpenDefault() { + const client = this._getPreferredClient(); + let proposedPlatform = null; + let webPlatform = null; + if (client) { + const matchingPlatforms = getMatchingPlatforms(client, this.platforms); + const selectedPlatforms = selectPlatforms(matchingPlatforms, this.preferences.platform); + if (selectedPlatforms.proposedPlatform !== selectedPlatforms.nativePlatform) { + // Do not auto-open web applications + return false; + } + proposedPlatform = selectedPlatforms.proposedPlatform; + webPlatform = selectedPlatforms.webPlatform; + + if (!client.getDeepLink(proposedPlatform, this._link)) { + // Client doesn't support deep links. We can't open it. + return false; + } + } + this.openDefaultViewModel = new OpenDefaultViewModel(this.childOptions({ + client, + link: this._link, + openLinkVM: this, + proposedPlatform, + webPlatform, + })); + this.openDefaultViewModel.tryOpenLink(); + return true; + } + + closeDefault() { + this.openDefaultViewModel = null; + this._activeOpen(); + } + + _activeOpen() { if (this.preferences.homeservers === null) { this._showServerConsent(); } else { @@ -41,18 +84,6 @@ export class OpenLinkViewModel extends ViewModel { } } - _tryLink() { - const matrixUrl = this._link.toMatrixUrl() - if (matrixUrl) { - this.tryingLink = true; - this.setTimeout(() => { - this.tryingLink = false; - this.emitChange(); - }, 1000); - this.openLink(matrixUrl); - } - } - _showServerConsent() { let servers = []; if (this.preferences.homeservers) { @@ -67,11 +98,16 @@ export class OpenLinkViewModel extends ViewModel { this._showLink(); } })); + this.emitChange(); } - async _showLink() { + _getPreferredClient() { const clientId = this.preferences.clientId || this._link.clientId; - const preferredClient = clientId ? this._clients.find(c => c.id === clientId) : null; + return clientId ? this._clients.find(c => c.id === clientId) : null; + } + + async _showLink() { + const preferredClient = this._getPreferredClient(); this.clientsViewModel = new ClientListViewModel(this.childOptions({ clients: this._clients, link: this._link, From c109293864a1fbadcec03db8ba5af7f03288ce59 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 26 Aug 2021 15:21:39 -0700 Subject: [PATCH 10/18] Implement fleshed out design --- css/open.css | 15 +++++++++++++++ src/open/OpenDefaultViewModel.js | 10 +++++++--- src/open/OpenLinkView.js | 29 +++++++++++++++++++++++++---- src/open/OpenLinkViewModel.js | 4 ++++ 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/css/open.css b/css/open.css index 326c8eca..9f351c77 100644 --- a/css/open.css +++ b/css/open.css @@ -68,6 +68,21 @@ limitations under the License. background-size: 85%; } +.OpeningClientView .clientIcon { + border-radius: 8px; + background-repeat: no-repeat; + background-size: cover; + width: 60px; + height: 60px; + overflow: hidden; + display: block; + margin-left: 16px; +} + +.OpeningClientView .timeoutOptions { + margin-top: 15px; +} + .OpeningClientView .spinner { margin-top: 15px; } diff --git a/src/open/OpenDefaultViewModel.js b/src/open/OpenDefaultViewModel.js index b187046c..3bbacaa6 100644 --- a/src/open/OpenDefaultViewModel.js +++ b/src/open/OpenDefaultViewModel.js @@ -32,6 +32,10 @@ export class OpenDefaultViewModel extends ViewModel { return this._client?.name; } + get iconUrl() { + return this._client?.icon; + } + get openingDefault() { return !this._client; } @@ -42,7 +46,7 @@ export class OpenDefaultViewModel extends ViewModel { } get webDeepLink() { - return this._client && this._webPlatform && this._client.getDeepLink(this._webPlatform); + return this._client && this._webPlatform && this._client.getDeepLink(this._webPlatform, this._link); } close() { @@ -50,7 +54,7 @@ export class OpenDefaultViewModel extends ViewModel { } tryOpenLink() { - this._trying = true; + this.trying = true; // TODO actually try opening link this.setTimeout(() => { if (this.autoRedirect) { @@ -58,7 +62,7 @@ export class OpenDefaultViewModel extends ViewModel { // bother with visual updates. this.close(); } else { - this._trying = false; + this.trying = false; this.emitChange(); } }, 1000); diff --git a/src/open/OpenLinkView.js b/src/open/OpenLinkView.js index b859b063..40259fe4 100644 --- a/src/open/OpenLinkView.js +++ b/src/open/OpenLinkView.js @@ -38,11 +38,32 @@ export class OpenLinkView extends TemplateView { class TryingLinkView extends TemplateView { render (t, vm) { + const explanation = vm.autoRedirect ? + "If this doesn't work, you will be redirected shortly." : + t.span(["Click ", t.strong(`"Open ${vm.name}"`), " to launch the desktop app."]); + const webLink = vm.webDeepLink ? + t.span(["You can also ", t.a({ + href: vm.webDeepLink, + target: "_blank", + rel: "noopener noreferrer", + }, `open ${vm.name} in your browser.`)]) : + []; + const timeoutOptions = t.span({ className: "timeoutOptions" }, [ + t.strong("Not working? "), + t.button({ className: "text", onClick: () => vm.tryOpenLink() }, "Try again"), + " or ", + t.button({ className: "text", onClick: () => vm.close() }, "select another app") + ]); + return t.div({ className: "OpeningClientView" }, [ - t.div({className: "defaultAvatar"}), - t.h1("Trying to open your default client..."), - t.span("If this doesn't work, you will be redirected shortly."), - t.div({className: "spinner"}), + vm.iconUrl ? t.img({ className: "clientIcon", src: vm.iconUrl }) : t.div({className: "defaultAvatar"}), + t.h1(vm.name ? `Opening ${vm.name}` : "Trying to open your default client..."), + explanation, + webLink, + t.map(vm => vm.trying, trying => trying ? + t.div({className: "spinner"}) : + timeoutOptions + ), ]); } } diff --git a/src/open/OpenLinkViewModel.js b/src/open/OpenLinkViewModel.js index 97fdac46..f9e927bc 100644 --- a/src/open/OpenLinkViewModel.js +++ b/src/open/OpenLinkViewModel.js @@ -73,6 +73,10 @@ export class OpenLinkViewModel extends ViewModel { closeDefault() { this.openDefaultViewModel = null; + // If no client was selected, this is a no-op. + // Otherwise, see ClientViewModel.back for some reasons + // why we do this. + this.preferences.setClient(undefined, undefined); this._activeOpen(); } From 1179db5ad90c4473f2df90a6a8d22dcfb4f6a3e7 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 26 Aug 2021 15:27:57 -0700 Subject: [PATCH 11/18] Actually try to open matrix links --- src/open/OpenDefaultViewModel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/open/OpenDefaultViewModel.js b/src/open/OpenDefaultViewModel.js index 3bbacaa6..59cbf4a9 100644 --- a/src/open/OpenDefaultViewModel.js +++ b/src/open/OpenDefaultViewModel.js @@ -54,8 +54,10 @@ export class OpenDefaultViewModel extends ViewModel { } tryOpenLink() { + this.openLink(this._client ? + this._client.getDeepLink(this._proposedPlatform, this._link) : + this._link.toMatrixUrl()); this.trying = true; - // TODO actually try opening link this.setTimeout(() => { if (this.autoRedirect) { // We're about to be closed so don't From ff2d3155022e72e687959a8eb94d094650ad24e2 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 26 Aug 2021 15:33:53 -0700 Subject: [PATCH 12/18] Rename OpenDefault to AutoOpen --- .../{OpenDefaultViewModel.js => AutoOpenViewModel.js} | 5 ++--- src/open/OpenLinkViewModel.js | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) rename src/open/{OpenDefaultViewModel.js => AutoOpenViewModel.js} (92%) diff --git a/src/open/OpenDefaultViewModel.js b/src/open/AutoOpenViewModel.js similarity index 92% rename from src/open/OpenDefaultViewModel.js rename to src/open/AutoOpenViewModel.js index 59cbf4a9..8598ef54 100644 --- a/src/open/OpenDefaultViewModel.js +++ b/src/open/AutoOpenViewModel.js @@ -15,9 +15,8 @@ limitations under the License. */ import {ViewModel} from "../utils/ViewModel.js"; -import {getMatchingPlatforms, selectPlatforms} from "./clients/index.js"; -export class OpenDefaultViewModel extends ViewModel { +export class AutoOpenViewModel extends ViewModel { constructor(options) { super(options); const {client, link, openLinkVM, proposedPlatform, webPlatform} = options; @@ -50,7 +49,7 @@ export class OpenDefaultViewModel extends ViewModel { } close() { - this._openLinkVM.closeDefault(); + this._openLinkVM.closeAutoOpen(); } tryOpenLink() { diff --git a/src/open/OpenLinkViewModel.js b/src/open/OpenLinkViewModel.js index f9e927bc..d2fefe6d 100644 --- a/src/open/OpenLinkViewModel.js +++ b/src/open/OpenLinkViewModel.js @@ -18,7 +18,7 @@ import {ViewModel} from "../utils/ViewModel.js"; import {ClientListViewModel} from "./ClientListViewModel.js"; import {ClientViewModel} from "./ClientViewModel.js"; import {PreviewViewModel} from "../preview/PreviewViewModel.js"; -import {OpenDefaultViewModel} from "./OpenDefaultViewModel.js"; +import {AutoOpenViewModel} from "./AutoOpenViewModel.js"; import {ServerConsentViewModel} from "./ServerConsentViewModel.js"; import {getLabelForLinkKind} from "../Link.js"; import {orderedUnique} from "../utils/unique.js"; @@ -36,12 +36,12 @@ export class OpenLinkViewModel extends ViewModel { this.clientsViewModel = null; this.previewLoading = false; this.tryingLink = false; - if (!this._tryOpenDefault()) { + if (!this._tryAutoOpen()) { this._activeOpen(); } } - _tryOpenDefault() { + _tryAutoOpen() { const client = this._getPreferredClient(); let proposedPlatform = null; let webPlatform = null; @@ -60,7 +60,7 @@ export class OpenLinkViewModel extends ViewModel { return false; } } - this.openDefaultViewModel = new OpenDefaultViewModel(this.childOptions({ + this.openDefaultViewModel = new AutoOpenViewModel(this.childOptions({ client, link: this._link, openLinkVM: this, @@ -71,7 +71,7 @@ export class OpenLinkViewModel extends ViewModel { return true; } - closeDefault() { + closeAutoOpen() { this.openDefaultViewModel = null; // If no client was selected, this is a no-op. // Otherwise, see ClientViewModel.back for some reasons From 64129caaff6c3ea99369711559fbeb9bf6794bec Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Fri, 27 Aug 2021 10:02:23 -0700 Subject: [PATCH 13/18] Make the 'try again' button a link --- src/open/AutoOpenViewModel.js | 16 ++++++++++++---- src/open/OpenLinkView.js | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/open/AutoOpenViewModel.js b/src/open/AutoOpenViewModel.js index 8598ef54..b96a1c2c 100644 --- a/src/open/AutoOpenViewModel.js +++ b/src/open/AutoOpenViewModel.js @@ -44,6 +44,12 @@ export class AutoOpenViewModel extends ViewModel { return this.openingDefault; } + get deepLink() { + return this._client ? + this._client.getDeepLink(this._proposedPlatform, this._link) : + this._link.toMatrixUrl() + } + get webDeepLink() { return this._client && this._webPlatform && this._client.getDeepLink(this._webPlatform, this._link); } @@ -52,10 +58,7 @@ export class AutoOpenViewModel extends ViewModel { this._openLinkVM.closeAutoOpen(); } - tryOpenLink() { - this.openLink(this._client ? - this._client.getDeepLink(this._proposedPlatform, this._link) : - this._link.toMatrixUrl()); + startSpinner() { this.trying = true; this.setTimeout(() => { if (this.autoRedirect) { @@ -69,4 +72,9 @@ export class AutoOpenViewModel extends ViewModel { }, 1000); this.emitChange(); } + + tryOpenLink() { + this.openLink(this.deepLink); + this.startSpinner(); + } } diff --git a/src/open/OpenLinkView.js b/src/open/OpenLinkView.js index 40259fe4..8937645b 100644 --- a/src/open/OpenLinkView.js +++ b/src/open/OpenLinkView.js @@ -50,7 +50,7 @@ class TryingLinkView extends TemplateView { []; const timeoutOptions = t.span({ className: "timeoutOptions" }, [ t.strong("Not working? "), - t.button({ className: "text", onClick: () => vm.tryOpenLink() }, "Try again"), + t.a({ href: vm.deepLink, onClick: () => vm.startSpinner() }, "Try again"), " or ", t.button({ className: "text", onClick: () => vm.close() }, "select another app") ]); From bc9e091d8b9259163698aa8d87415285e5c45275 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Fri, 27 Aug 2021 10:08:54 -0700 Subject: [PATCH 14/18] Make less assumptions about auto redirect etc. in View code --- src/open/OpenLinkView.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/open/OpenLinkView.js b/src/open/OpenLinkView.js index 8937645b..e7f74509 100644 --- a/src/open/OpenLinkView.js +++ b/src/open/OpenLinkView.js @@ -38,9 +38,8 @@ export class OpenLinkView extends TemplateView { class TryingLinkView extends TemplateView { render (t, vm) { - const explanation = vm.autoRedirect ? - "If this doesn't work, you will be redirected shortly." : - t.span(["Click ", t.strong(`"Open ${vm.name}"`), " to launch the desktop app."]); + const explanation = vm.name ? t.span(["Click ", t.strong(`"Open ${vm.name}"`), " to launch the desktop app."]) : []; + const redirectNotice = vm.autoRedirect ? "If this doesn't work, you will be redirected shortly." : []; const webLink = vm.webDeepLink ? t.span(["You can also ", t.a({ href: vm.webDeepLink, @@ -59,6 +58,7 @@ class TryingLinkView extends TemplateView { vm.iconUrl ? t.img({ className: "clientIcon", src: vm.iconUrl }) : t.div({className: "defaultAvatar"}), t.h1(vm.name ? `Opening ${vm.name}` : "Trying to open your default client..."), explanation, + redirectNotice, webLink, t.map(vm => vm.trying, trying => trying ? t.div({className: "spinner"}) : From b12f2fb074b49fde925488f5eaf2da3eb439315c Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 2 Sep 2021 11:16:34 -0700 Subject: [PATCH 15/18] Minor design fixes (line height, punctuation) --- css/open.css | 1 + src/open/OpenLinkView.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/css/open.css b/css/open.css index 9f351c77..88f3c7ea 100644 --- a/css/open.css +++ b/css/open.css @@ -57,6 +57,7 @@ limitations under the License. text-align: center; flex-direction: column; margin: 0; + line-height: 150%; } .OpeningClientView .defaultAvatar { diff --git a/src/open/OpenLinkView.js b/src/open/OpenLinkView.js index e7f74509..feeb44af 100644 --- a/src/open/OpenLinkView.js +++ b/src/open/OpenLinkView.js @@ -51,7 +51,7 @@ class TryingLinkView extends TemplateView { t.strong("Not working? "), t.a({ href: vm.deepLink, onClick: () => vm.startSpinner() }, "Try again"), " or ", - t.button({ className: "text", onClick: () => vm.close() }, "select another app") + t.button({ className: "text", onClick: () => vm.close() }, "select another app.") ]); return t.div({ className: "OpeningClientView" }, [ From 890673d8ac437088d6e50218a85c3c2ead01e17c Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 2 Sep 2021 11:22:42 -0700 Subject: [PATCH 16/18] Avoid opening iOS --- src/open/OpenLinkViewModel.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/open/OpenLinkViewModel.js b/src/open/OpenLinkViewModel.js index d2fefe6d..c8d65a38 100644 --- a/src/open/OpenLinkViewModel.js +++ b/src/open/OpenLinkViewModel.js @@ -23,6 +23,7 @@ import {ServerConsentViewModel} from "./ServerConsentViewModel.js"; import {getLabelForLinkKind} from "../Link.js"; import {orderedUnique} from "../utils/unique.js"; import {getMatchingPlatforms, selectPlatforms} from "./clients/index.js"; +import {Platform} from "../Platform.js"; export class OpenLinkViewModel extends ViewModel { constructor(options) { @@ -59,6 +60,11 @@ export class OpenLinkViewModel extends ViewModel { // Client doesn't support deep links. We can't open it. return false; } + } else { + if (this.platforms.includes(Platform.iOS)) { + // Do not try to auto-open links on iOS because of the scary warning. + return false; + } } this.openDefaultViewModel = new AutoOpenViewModel(this.childOptions({ client, From 86196d4c83b62324a12ae969248370f884f9ece5 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 2 Sep 2021 11:25:44 -0700 Subject: [PATCH 17/18] Add tentative copy change for 'click' and 'desktop' --- src/open/OpenLinkView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/open/OpenLinkView.js b/src/open/OpenLinkView.js index feeb44af..4257e83a 100644 --- a/src/open/OpenLinkView.js +++ b/src/open/OpenLinkView.js @@ -38,7 +38,7 @@ export class OpenLinkView extends TemplateView { class TryingLinkView extends TemplateView { render (t, vm) { - const explanation = vm.name ? t.span(["Click ", t.strong(`"Open ${vm.name}"`), " to launch the desktop app."]) : []; + const explanation = vm.name ? t.span(["Select ", t.strong(`"Open ${vm.name}"`), " to launch the app."]) : []; const redirectNotice = vm.autoRedirect ? "If this doesn't work, you will be redirected shortly." : []; const webLink = vm.webDeepLink ? t.span(["You can also ", t.a({ From 9a0d21df781efb748cd2dc8661049e9e9ade701b Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 16 Sep 2021 21:44:07 -0700 Subject: [PATCH 18/18] Avoid using empty list to represent 'no element' --- src/open/OpenLinkView.js | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/open/OpenLinkView.js b/src/open/OpenLinkView.js index 4257e83a..4bc366b7 100644 --- a/src/open/OpenLinkView.js +++ b/src/open/OpenLinkView.js @@ -38,33 +38,37 @@ export class OpenLinkView extends TemplateView { class TryingLinkView extends TemplateView { render (t, vm) { - const explanation = vm.name ? t.span(["Select ", t.strong(`"Open ${vm.name}"`), " to launch the app."]) : []; - const redirectNotice = vm.autoRedirect ? "If this doesn't work, you will be redirected shortly." : []; - const webLink = vm.webDeepLink ? - t.span(["You can also ", t.a({ + const children = [ + vm.iconUrl ? t.img({ className: "clientIcon", src: vm.iconUrl }) : t.div({className: "defaultAvatar"}), + t.h1(vm.name ? `Opening ${vm.name}` : "Trying to open your default client..."), + ] + if (vm.name) { + children.push(t.span(["Select ", t.strong(`"Open ${vm.name}"`), " to launch the app."])); + } + if (vm.autoRedirect) { + children.push("If this doesn't work, you will be redirected shortly."); + } + if (vm.webDeepLink) { + children.push(t.span(["You can also ", t.a({ href: vm.webDeepLink, target: "_blank", rel: "noopener noreferrer", - }, `open ${vm.name} in your browser.`)]) : - []; + }, `open ${vm.name} in your browser.`)])); + } const timeoutOptions = t.span({ className: "timeoutOptions" }, [ t.strong("Not working? "), t.a({ href: vm.deepLink, onClick: () => vm.startSpinner() }, "Try again"), " or ", t.button({ className: "text", onClick: () => vm.close() }, "select another app.") ]); - - return t.div({ className: "OpeningClientView" }, [ - vm.iconUrl ? t.img({ className: "clientIcon", src: vm.iconUrl }) : t.div({className: "defaultAvatar"}), - t.h1(vm.name ? `Opening ${vm.name}` : "Trying to open your default client..."), - explanation, - redirectNotice, - webLink, + children.push( t.map(vm => vm.trying, trying => trying ? t.div({className: "spinner"}) : timeoutOptions ), - ]); + ); + + return t.div({ className: "OpeningClientView" }, children); } }