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);
|
||||
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",
|
||||
);
|
||||
|
||||
function idToPath(identifier) {
|
||||
return encodeURIComponent(identifier.substring(1));
|
||||
}
|
||||
|
||||
function asPrefix(identifierKind) {
|
||||
switch (identifierKind) {
|
||||
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) {
|
||||
const prefix = "web-instance[";
|
||||
const postfix = "]";
|
||||
|
@ -183,4 +197,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 = 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({
|
||||
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,
|
||||
|
|
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 {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();
|
||||
|
|
|
@ -22,14 +22,56 @@ 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.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) {
|
||||
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 {
|
||||
render(t, vm) {
|
||||
return t.div([
|
||||
|
|
|
@ -18,9 +18,12 @@ import {ViewModel} from "../utils/ViewModel.js";
|
|||
import {ClientListViewModel} from "./ClientListViewModel.js";
|
||||
import {ClientViewModel} from "./ClientViewModel.js";
|
||||
import {PreviewViewModel} from "../preview/PreviewViewModel.js";
|
||||
import {AutoOpenViewModel} from "./AutoOpenViewModel.js";
|
||||
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) {
|
||||
|
@ -28,10 +31,62 @@ 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;
|
||||
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) {
|
||||
this._showServerConsent();
|
||||
} else {
|
||||
|
@ -53,11 +108,16 @@ export class OpenLinkViewModel extends ViewModel {
|
|||
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() {
|
||||
const clientId = this.preferences.clientId || this._link.clientId;
|
||||
const preferredClient = clientId ? this._clients.find(c => c.id === clientId) : null;
|
||||
const preferredClient = this._getPreferredClient();
|
||||
this.clientsViewModel = new ClientListViewModel(this.childOptions({
|
||||
clients: this._clients,
|
||||
link: this._link,
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue