-
+ matrix.to - you're invited to chat on matrix
+
diff --git a/src/RootViewModel.js b/src/RootViewModel.js
index 099b4e2..9c1a43a 100644
--- a/src/RootViewModel.js
+++ b/src/RootViewModel.js
@@ -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();
}
diff --git a/src/client/ClientListView.js b/src/client/ClientListView.js
new file mode 100644
index 0000000..ef3ae16
--- /dev/null
+++ b/src/client/ClientListView.js
@@ -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)
+ ]);
+ }
+}
diff --git a/src/client/ClientListViewModel.js b/src/client/ClientListViewModel.js
new file mode 100644
index 0000000..c998075
--- /dev/null
+++ b/src/client/ClientListViewModel.js
@@ -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})));
+ }
+}
diff --git a/src/client/ClientView.js b/src/client/ClientView.js
new file mode 100644
index 0000000..d5c789e
--- /dev/null
+++ b/src/client/ClientView.js
@@ -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);
+ }))
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/src/client/ClientViewModel.js b/src/client/ClientViewModel.js
new file mode 100644
index 0000000..1ad7c23
--- /dev/null
+++ b/src/client/ClientViewModel.js
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/client/Platform.js b/src/client/Platform.js
new file mode 100644
index 0000000..a5b3c15
--- /dev/null
+++ b/src/client/Platform.js
@@ -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;
+}
\ No newline at end of file
diff --git a/src/client/clients/Element.js b/src/client/clients/Element.js
new file mode 100644
index 0000000..13f5db5
--- /dev/null
+++ b/src/client/clients/Element.js
@@ -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")];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/client/types.js b/src/client/types.js
new file mode 100644
index 0000000..b6fda03
--- /dev/null
+++ b/src/client/types.js
@@ -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}`;
+ }
+}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
index 2ce7f76..de41b5d 100644
--- a/src/main.js
+++ b/src/main.js
@@ -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);
diff --git a/src/preview/PreviewView.js b/src/preview/PreviewView.js
index 3dc614b..f4c217b 100644
--- a/src/preview/PreviewView.js
+++ b/src/preview/PreviewView.js
@@ -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)
])
]);
}
diff --git a/src/preview/PreviewViewModel.js b/src/preview/PreviewViewModel.js
index 5911c02..e232ca4 100644
--- a/src/preview/PreviewViewModel.js
+++ b/src/preview/PreviewViewModel.js
@@ -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();
+ }
}
\ No newline at end of file
diff --git a/src/utils/ViewModel.js b/src/utils/ViewModel.js
index 4cc14e1..3cc34f2 100644
--- a/src/utils/ViewModel.js
+++ b/src/utils/ViewModel.js
@@ -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);
}
}
\ No newline at end of file