very basic client list code

This commit is contained in:
Bruno Windels
2020-11-30 21:05:26 +01:00
parent eded08595a
commit 8659594c22
16 changed files with 482 additions and 15 deletions
+6 -1
View File
@@ -17,6 +17,7 @@ limitations under the License.
import {Link} from "./Link.js";
import {ViewModel} from "./utils/ViewModel.js";
import {PreviewViewModel} from "./preview/PreviewViewModel.js";
import {Element} from "./client/clients/Element.js";
export class RootViewModel extends ViewModel {
constructor(options) {
@@ -28,9 +29,13 @@ export class RootViewModel extends ViewModel {
_updateChildVMs(oldLink) {
if (this.link) {
if (!oldLink || !oldLink.equals(this.link)) {
const element = new Element();
this.previewViewModel = new PreviewViewModel(this.childOptions({
link: this.link,
consentedServers: this.link.servers
consentedServers: this.link.servers,
// preferredClient: element,
// preferredPlatform: this.platforms[0],
clients: [element]
}));
this.previewViewModel.load();
}
+28
View File
@@ -0,0 +1,28 @@
/*
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 {TemplateView} from "../utils/TemplateView.js";
import {ClientView} from "./ClientView.js";
export class ClientListView extends TemplateView {
render(t, vm) {
const clients = vm.clients.map(clientViewModel => t.view(new ClientView(clientViewModel)));
return t.div({className: "ClientListView"}, [
t.h3("You need an app to continue"),
t.ul({className: "ClientListView"}, clients)
]);
}
}
+27
View File
@@ -0,0 +1,27 @@
/*
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 {isWebPlatform, Platform} from "./Platform.js";
import {ClientViewModel} from "./ClientViewModel.js";
import {ViewModel} from "../utils/ViewModel.js";
export class ClientListViewModel extends ViewModel {
constructor(options) {
super(options);
const {clients, link} = options;
this.clients = clients.map(client => new ClientViewModel(this.childOptions({client, link})));
}
}
+34
View File
@@ -0,0 +1,34 @@
/*
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 {TemplateView} from "../utils/TemplateView.js";
export class ClientView extends TemplateView {
render(t, vm) {
return t.li({className: "ClientView"}, [
t.div({className: "header"}, [
t.div({className: "description"}, [
t.h3(vm.name),
t.p(vm.description),
]),
t.div({className: `icon ${vm.clientId}`})
]),
t.div({className: "actions"}, vm.actions.map(a => {
return t.a({href: a.url, className: a.kind, rel: "noopener noreferrer", onClick: () => a.activated()}, a.label);
}))
]);
}
}
+66
View File
@@ -0,0 +1,66 @@
/*
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 {isWebPlatform, Platform} from "./Platform.js";
import {ViewModel} from "../utils/ViewModel.js";
export class ClientViewModel extends ViewModel {
constructor(options) {
super(options);
const {client, link} = options;
this._client = client;
const supportedPlatforms = client.platforms;
const matchingPlatforms = this.platforms.filter(p => {
return supportedPlatforms.includes(p);
});
const nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p));
const webPlatform = this.platforms.find(p => isWebPlatform(p));
this.actions = this._createActions(client, link, nativePlatform, webPlatform);
this.name = this._client.getName(nativePlatform || webPlatform);
}
_createActions(client, link, nativePlatform, webPlatform) {
let actions = [];
if (nativePlatform) {
const nativeActions = client.getInstallLinks(nativePlatform).map(installLink => {
return {
label: installLink.description,
url: installLink.createInstallURL(link),
kind: installLink.channelId,
activated() {},
};
});
actions.push(...nativeActions);
}
if (webPlatform) {
actions.push({
label: `Or open in ${client.getName(webPlatform)}`,
url: client.getDeepLink(webPlatform, link),
kind: "open-in-web",
activated() {},
});
}
return actions;
}
get description() {
return this._client.description;
}
get clientId() {
return this._client.id;
}
}
+36
View File
@@ -0,0 +1,36 @@
/*
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 {createEnum} from "../utils/enum.js";
export const Platform = createEnum(
"DesktopWeb",
"MobileWeb",
"Android",
"iOS",
"Windows",
"macOS",
"Linux"
);
export function guessApplicablePlatforms(userAgent) {
// use https://github.com/faisalman/ua-parser-js to guess, and pass as RootVM options
return [Platform.DesktopWeb, Platform.Linux];
}
export function isWebPlatform(p) {
return p === Platform.DesktopWeb || p === Platform.MobileWeb;
}
+81
View File
@@ -0,0 +1,81 @@
/*
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 {Maturity, Platform, LinkKind,
FDroidLink, AppleStoreLink, PlayStoreLink, WebsiteLink} from "../types.js";
/**
* Information on how to deep link to a given matrix client.
*/
export class Element {
/* should only contain alphanumerical and -_, no dots (needs to be usable as css class) */
get id() { return "element-io"; }
get platforms() {
return [
Platform.Android, Platform.iOS,
Platform.Windows, Platform.macOS, Platform.Linux,
Platform.DesktopWeb, Platform.MobileWeb
];
}
get description() { return 'Fully-featured Matrix client'; }
getMaturity(platform) { return Maturity.Stable; }
getLinkSupport(platform, link) { return true; }
getDeepLink(platform, link) {
let fragmentPath;
switch (link.kind) {
case LinkKind.User:
fragmentPath = `user/${link.identifier}`;
break;
case LinkKind.Room:
fragmentPath = `room/${link.identifier}`;
break;
case LinkKind.Group:
fragmentPath = `group/${link.identifier}`;
break;
case LinkKind.Event:
fragmentPath = `room/${link.identifier}/${link.eventId}`;
break;
}
if (platform === Platform.DesktopWeb || platform === Platform.MobileWeb || platform === Platform.iOS) {
return `https://app.element.io/#/${fragmentPath}`;
} else {
return `element://${fragmentPath}`;
}
}
getLinkInstructions(platform, link) {}
getName(platform) {
if (platform === Platform.DesktopWeb || platform === Platform.MobileWeb) {
return "Element Web";
} else {
return "Element";
}
}
getInstallLinks(platform) {
switch (platform) {
case Platform.iOS: return [new AppleStoreLink('vector', 'id1083446067')];
case Platform.Android: return [new PlayStoreLink('im.vector.app'), new FDroidLink('im.vector.app')];
default: return [new WebsiteLink("https://element.io/get-started")];
}
}
}
+94
View File
@@ -0,0 +1,94 @@
/*
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 {createEnum} from "../utils/enum.js";
export const Maturity = createEnum("Alpha", "Beta", "Stable");
export {LinkKind} from "../Link.js";
export {Platform} from "./Platform.js";
export class AppleStoreLink {
constructor(org, appId) {
this._org = org;
this._appId = appId;
}
createInstallURL(link) {
return `https://apps.apple.com/app/${encodeURIComponent(this._org)}/${encodeURIComponent(this._appId)}`;
}
get channelId() {
return "apple-app-store";
}
get description() {
return "Download on the App Store";
}
}
export class PlayStoreLink {
constructor(appId) {
this._appId = appId;
}
createInstallURL(link) {
return `https://play.google.com/store/apps/details?id=${encodeURIComponent(this._appId)}&referrer=${encodeURIComponent(link.identifier)}`;
}
get channelId() {
return "play-store";
}
get description() {
return "Get it on Google Play";
}
}
export class FDroidLink {
constructor(appId) {
this._appId = appId;
}
createInstallURL(link) {
return `https://f-droid.org/packages/${encodeURIComponent(this._appId)}`;
}
get channelId() {
return "fdroid";
}
get description() {
return "Get it on F-Droid";
}
}
export class WebsiteLink {
constructor(url) {
this._url = url;
}
createInstallURL(link) {
return this._url;
}
get channelId() {
return "website";
}
get description() {
return `Download from ${new URL(this._url).hostname}`;
}
}
+6 -1
View File
@@ -1,9 +1,14 @@
import {xhrRequest} from "./utils/xhr.js";
import {RootViewModel} from "./RootViewModel.js";
import {RootView} from "./RootView.js";
import {guessApplicablePlatforms} from "./client/Platform.js";
export async function main(container) {
const vm = new RootViewModel({request: xhrRequest});
const vm = new RootViewModel({
request: xhrRequest,
openLink: url => location.href = url,
platforms: guessApplicablePlatforms(navigator.userAgent),
});
vm.updateHash(location.hash);
window.__rootvm = vm;
const view = new RootView(vm);
+12 -7
View File
@@ -15,18 +15,23 @@ limitations under the License.
*/
import {TemplateView} from "../utils/TemplateView.js";
import {ClientListView} from "../client/ClientListView.js";
export class PreviewView extends TemplateView {
render(t, vm) {
return t.div({className: "PreviewView card"}, [
t.h2({className: {hidden: vm => !vm.loading}}, "Loading preview…"),
t.div({className: {preview: true, hidden: vm => vm.loading}}, [
t.p(t.img({className: "avatar", src: vm => vm.avatarUrl})),
t.div({className: "profileInfo"}, [
t.h2(vm => vm.name),
t.p(vm => vm.identifier),
t.p(["Preview from ", vm => vm.previewDomain]),
])
t.div({className: {hidden: vm => vm.loading}}, [
t.div({className: "preview"}, [
t.p(t.img({className: "avatar", src: vm => vm.avatarUrl})),
t.div({className: "profileInfo"}, [
t.h2(vm => vm.name),
t.p(vm => vm.identifier),
t.p(["Preview from ", vm => vm.previewDomain]),
]),
]),
t.p({hidden: vm => !!vm.clientsViewModel}, t.button({onClick: () => vm.accept()}, vm => vm.acceptLabel)),
t.mapView(vm => vm.clientsViewModel, vm => vm ? new ClientListView(vm) : null)
])
]);
}
+36 -1
View File
@@ -17,17 +17,28 @@ limitations under the License.
import {LinkKind} from "../Link.js";
import {ViewModel} from "../utils/ViewModel.js";
import {resolveServer} from "./HomeServer.js";
import {ClientListViewModel} from "../client/ClientListViewModel.js";
export class PreviewViewModel extends ViewModel {
constructor(options) {
super(options);
const {link, consentedServers} = options;
const {
link, consentedServers,
preferredClient, preferredPlatform, clients
} = options;
this._link = link;
this._consentedServers = consentedServers;
this._preferredClient = preferredClient;
// used to differentiate web from native if a client supports both
this._preferredPlatform = preferredPlatform;
this._clients = clients;
this.loading = false;
this.name = null;
this.avatarUrl = null;
this.previewDomain = null;
this.clientsViewModel = null;
this.acceptInstructions = null;
}
async load() {
@@ -62,4 +73,28 @@ export class PreviewViewModel extends ViewModel {
get identifier() {
return this._link.identifier;
}
get acceptLabel() {
if (this._preferredClient) {
return `Open in ${this._preferredClient.getName(this._preferredPlatform)}`;
} else {
return "Choose app";
}
}
accept() {
if (this._preferredClient) {
if (this._preferredClient.getLinkSupport(this._preferredPlatform, this._link)) {
const deepLink = this._preferredClient.getDeepLink(this._preferredPlatform, this._link);
this.openLink(deepLink);
// show "looks like you don't have the native app installed"
} else {
this.acceptInstructions = this._preferredClient.getLinkInstructions(this._preferredPlatform, this._link);
}
} else {
this.clientsViewModel = new ClientListViewModel(this.childOptions({clients: this._clients, link: this._link}));
// show client list
}
this.emitChange();
}
}
+8 -4
View File
@@ -60,11 +60,15 @@ export class ViewModel extends EventEmitter {
this.emit("change");
}
get request() {
return this._options.request;
}
get request() { return this._options.request; }
get openLink() { return this._options.openLink; }
get platforms() { return this._options.platforms; }
childOptions(options = {}) {
return Object.assign({request: this.request}, options);
return Object.assign({
request: this.request,
openLink: this.openLink,
platforms: this.platforms,
}, options);
}
}