Compare commits
18 commits
main
...
danilafe/o
Author | SHA1 | Date | |
---|---|---|---|
|
9a0d21df78 | ||
|
86196d4c83 | ||
|
890673d8ac | ||
|
b12f2fb074 | ||
|
bc9e091d8b | ||
|
64129caaff | ||
|
ff2d315502 | ||
|
1179db5ad9 | ||
|
c109293864 | ||
|
a781c527fd | ||
|
820a090a71 | ||
|
94f0883fec | ||
|
ee820b6ca1 | ||
|
b8e07cda7e | ||
|
c26b8dcec1 | ||
|
b239f49580 | ||
|
b17ce2ee13 | ||
|
bfcac9fafb |
9 changed files with 277 additions and 18 deletions
37
css/open.css
37
css/open.css
|
@ -50,3 +50,40 @@ limitations under the License.
|
||||||
border-bottom: 1px solid var(--grey);
|
border-bottom: 1px solid var(--grey);
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.OpeningClientView {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpeningClientView .defaultAvatar {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background-image: url('../images/chat-icon.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
25
src/Link.js
25
src/Link.js
|
@ -30,6 +30,10 @@ export const IdentifierKind = createEnum(
|
||||||
"GroupId",
|
"GroupId",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function idToPath(identifier) {
|
||||||
|
return encodeURIComponent(identifier.substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
function asPrefix(identifierKind) {
|
function asPrefix(identifierKind) {
|
||||||
switch (identifierKind) {
|
switch (identifierKind) {
|
||||||
case IdentifierKind.RoomId: return "!";
|
case IdentifierKind.RoomId: return "!";
|
||||||
|
@ -40,6 +44,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) {
|
function getWebInstanceMap(queryParams) {
|
||||||
const prefix = "web-instance[";
|
const prefix = "web-instance[";
|
||||||
const postfix = "]";
|
const postfix = "]";
|
||||||
|
@ -183,4 +197,15 @@ export class Link {
|
||||||
return `/${this.identifier}`;
|
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 = idToPath(this.identifier);
|
||||||
|
const suffix = this.eventId ? `/e/${idToPath(this.eventId)}` : "";
|
||||||
|
return `matrix:${prefix}/${identifier}${suffix}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ export async function main(container) {
|
||||||
const vm = new RootViewModel({
|
const vm = new RootViewModel({
|
||||||
request: xhrRequest,
|
request: xhrRequest,
|
||||||
openLink: url => location.href = url,
|
openLink: url => location.href = url,
|
||||||
|
setTimeout: (f, time) => window.setTimeout(f, time),
|
||||||
platforms: guessApplicablePlatforms(navigator.userAgent, navigator.platform),
|
platforms: guessApplicablePlatforms(navigator.userAgent, navigator.platform),
|
||||||
preferences: new Preferences(window.localStorage),
|
preferences: new Preferences(window.localStorage),
|
||||||
origin: location.origin,
|
origin: location.origin,
|
||||||
|
|
80
src/open/AutoOpenViewModel.js
Normal file
80
src/open/AutoOpenViewModel.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
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";
|
||||||
|
|
||||||
|
export class AutoOpenViewModel 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 iconUrl() {
|
||||||
|
return this._client?.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get openingDefault() {
|
||||||
|
return !this._client;
|
||||||
|
}
|
||||||
|
|
||||||
|
get autoRedirect() {
|
||||||
|
// Only auto-redirect when a preferred client hasn't been set.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this._openLinkVM.closeAutoOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
startSpinner() {
|
||||||
|
this.trying = true;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
tryOpenLink() {
|
||||||
|
this.openLink(this.deepLink);
|
||||||
|
this.startSpinner();
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,14 +17,7 @@ limitations under the License.
|
||||||
import {isWebPlatform, isDesktopPlatform, Platform} from "../Platform.js";
|
import {isWebPlatform, isDesktopPlatform, Platform} from "../Platform.js";
|
||||||
import {ViewModel} from "../utils/ViewModel.js";
|
import {ViewModel} from "../utils/ViewModel.js";
|
||||||
import {IdentifierKind} from "../Link.js";
|
import {IdentifierKind} from "../Link.js";
|
||||||
|
import {getMatchingPlatforms, selectPlatforms} from "./clients/index.js";
|
||||||
function getMatchingPlatforms(client, supportedPlatforms) {
|
|
||||||
const clientPlatforms = client.platforms;
|
|
||||||
const matchingPlatforms = supportedPlatforms.filter(p => {
|
|
||||||
return clientPlatforms.includes(p);
|
|
||||||
});
|
|
||||||
return matchingPlatforms;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ClientViewModel extends ViewModel {
|
export class ClientViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
@ -40,10 +33,10 @@ export class ClientViewModel extends ViewModel {
|
||||||
|
|
||||||
_update() {
|
_update() {
|
||||||
const matchingPlatforms = getMatchingPlatforms(this._client, this.platforms);
|
const matchingPlatforms = getMatchingPlatforms(this._client, this.platforms);
|
||||||
this._webPlatform = matchingPlatforms.find(p => isWebPlatform(p));
|
const {proposedPlatform, nativePlatform, webPlatform} = selectPlatforms(matchingPlatforms, this.preferences.platform);
|
||||||
this._nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p));
|
this._nativePlatform = nativePlatform;
|
||||||
const preferredPlatform = matchingPlatforms.find(p => p === this.preferences.platform);
|
this._webPlatform = webPlatform;
|
||||||
this._proposedPlatform = preferredPlatform || this._nativePlatform || this._webPlatform;
|
this._proposedPlatform = proposedPlatform;
|
||||||
|
|
||||||
this.openActions = this._createOpenActions();
|
this.openActions = this._createOpenActions();
|
||||||
this.installActions = this._createInstallActions();
|
this.installActions = this._createInstallActions();
|
||||||
|
|
|
@ -22,14 +22,56 @@ import {ServerConsentView} from "./ServerConsentView.js";
|
||||||
export class OpenLinkView extends TemplateView {
|
export class OpenLinkView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.div({className: "OpenLinkView card"}, [
|
return t.div({className: "OpenLinkView card"}, [
|
||||||
t.mapView(vm => vm.previewViewModel, previewVM => previewVM ?
|
t.mapView(vm => [ vm.openDefaultViewModel, vm.previewViewModel ], ([openDefaultVM, previewVM]) => {
|
||||||
new ShowLinkView(vm) :
|
if (openDefaultVM) {
|
||||||
new ServerConsentView(vm.serverConsentViewModel)
|
return new TryingLinkView(openDefaultVM)
|
||||||
),
|
} else if (previewVM) {
|
||||||
|
return new ShowLinkView(vm);
|
||||||
|
} else {
|
||||||
|
return new ServerConsentView(vm.serverConsentViewModel);
|
||||||
|
}
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TryingLinkView extends TemplateView {
|
||||||
|
render (t, vm) {
|
||||||
|
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.`)]));
|
||||||
|
}
|
||||||
|
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.")
|
||||||
|
]);
|
||||||
|
children.push(
|
||||||
|
t.map(vm => vm.trying, trying => trying ?
|
||||||
|
t.div({className: "spinner"}) :
|
||||||
|
timeoutOptions
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return t.div({ className: "OpeningClientView" }, children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ShowLinkView extends TemplateView {
|
class ShowLinkView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.div([
|
return t.div([
|
||||||
|
|
|
@ -18,9 +18,12 @@ import {ViewModel} from "../utils/ViewModel.js";
|
||||||
import {ClientListViewModel} from "./ClientListViewModel.js";
|
import {ClientListViewModel} from "./ClientListViewModel.js";
|
||||||
import {ClientViewModel} from "./ClientViewModel.js";
|
import {ClientViewModel} from "./ClientViewModel.js";
|
||||||
import {PreviewViewModel} from "../preview/PreviewViewModel.js";
|
import {PreviewViewModel} from "../preview/PreviewViewModel.js";
|
||||||
|
import {AutoOpenViewModel} from "./AutoOpenViewModel.js";
|
||||||
import {ServerConsentViewModel} from "./ServerConsentViewModel.js";
|
import {ServerConsentViewModel} from "./ServerConsentViewModel.js";
|
||||||
import {getLabelForLinkKind} from "../Link.js";
|
import {getLabelForLinkKind} from "../Link.js";
|
||||||
import {orderedUnique} from "../utils/unique.js";
|
import {orderedUnique} from "../utils/unique.js";
|
||||||
|
import {getMatchingPlatforms, selectPlatforms} from "./clients/index.js";
|
||||||
|
import {Platform} from "../Platform.js";
|
||||||
|
|
||||||
export class OpenLinkViewModel extends ViewModel {
|
export class OpenLinkViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
@ -28,10 +31,62 @@ export class OpenLinkViewModel extends ViewModel {
|
||||||
const {clients, link} = options;
|
const {clients, link} = options;
|
||||||
this._link = link;
|
this._link = link;
|
||||||
this._clients = clients;
|
this._clients = clients;
|
||||||
|
this.openDefaultViewModel = null;
|
||||||
this.serverConsentViewModel = null;
|
this.serverConsentViewModel = null;
|
||||||
this.previewViewModel = null;
|
this.previewViewModel = null;
|
||||||
this.clientsViewModel = null;
|
this.clientsViewModel = null;
|
||||||
this.previewLoading = false;
|
this.previewLoading = false;
|
||||||
|
this.tryingLink = false;
|
||||||
|
if (!this._tryAutoOpen()) {
|
||||||
|
this._activeOpen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_tryAutoOpen() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
} 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,
|
||||||
|
link: this._link,
|
||||||
|
openLinkVM: this,
|
||||||
|
proposedPlatform,
|
||||||
|
webPlatform,
|
||||||
|
}));
|
||||||
|
this.openDefaultViewModel.tryOpenLink();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAutoOpen() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
_activeOpen() {
|
||||||
if (this.preferences.homeservers === null) {
|
if (this.preferences.homeservers === null) {
|
||||||
this._showServerConsent();
|
this._showServerConsent();
|
||||||
} else {
|
} else {
|
||||||
|
@ -53,11 +108,16 @@ export class OpenLinkViewModel extends ViewModel {
|
||||||
this._showLink();
|
this._showLink();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPreferredClient() {
|
||||||
|
const clientId = this.preferences.clientId || this._link.clientId;
|
||||||
|
return clientId ? this._clients.find(c => c.id === clientId) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _showLink() {
|
async _showLink() {
|
||||||
const clientId = this.preferences.clientId || this._link.clientId;
|
const preferredClient = this._getPreferredClient();
|
||||||
const preferredClient = clientId ? this._clients.find(c => c.id === clientId) : null;
|
|
||||||
this.clientsViewModel = new ClientListViewModel(this.childOptions({
|
this.clientsViewModel = new ClientListViewModel(this.childOptions({
|
||||||
clients: this._clients,
|
clients: this._clients,
|
||||||
link: this._link,
|
link: this._link,
|
||||||
|
|
|
@ -21,6 +21,25 @@ import {Fractal} from "./Fractal.js";
|
||||||
import {Quaternion} from "./Quaternion.js";
|
import {Quaternion} from "./Quaternion.js";
|
||||||
import {Tensor} from "./Tensor.js";
|
import {Tensor} from "./Tensor.js";
|
||||||
import {Fluffychat} from "./Fluffychat.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() {
|
export function createClients() {
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -63,6 +63,7 @@ export class ViewModel extends EventEmitter {
|
||||||
get request() { return this._options.request; }
|
get request() { return this._options.request; }
|
||||||
get origin() { return this._options.origin; }
|
get origin() { return this._options.origin; }
|
||||||
get openLink() { return this._options.openLink; }
|
get openLink() { return this._options.openLink; }
|
||||||
|
get setTimeout() { return this._options.setTimeout; }
|
||||||
get platforms() { return this._options.platforms; }
|
get platforms() { return this._options.platforms; }
|
||||||
get preferences() { return this._options.preferences; }
|
get preferences() { return this._options.preferences; }
|
||||||
|
|
||||||
|
@ -71,6 +72,7 @@ export class ViewModel extends EventEmitter {
|
||||||
request: this.request,
|
request: this.request,
|
||||||
origin: this.origin,
|
origin: this.origin,
|
||||||
openLink: this.openLink,
|
openLink: this.openLink,
|
||||||
|
setTimeout: this.setTimeout,
|
||||||
platforms: this.platforms,
|
platforms: this.platforms,
|
||||||
preferences: this.preferences,
|
preferences: this.preferences,
|
||||||
}, options);
|
}, options);
|
||||||
|
|
Loading…
Reference in a new issue