implement flow refinements as discussed with Nad,add text client support

This commit is contained in:
Bruno Windels 2020-12-01 19:01:15 +01:00
parent cf489284c9
commit 1caa9b1bf3
8 changed files with 130 additions and 86 deletions

View file

@ -70,19 +70,14 @@ textarea {
padding: 2rem; padding: 2rem;
} }
.PreviewView .preview {
text-align: center;
}
.PreviewView .avatar { .PreviewView .avatar {
border-radius: 100%; border-radius: 100%;
} }
.PreviewView .preview {
display: flex;
}
.PreviewView .profileInfo {
flex: 1;
margin-left: 12px;
}
.hidden { .hidden {
display: none !important; display: none !important;
} }
@ -139,24 +134,30 @@ button.text:hover {
cursor: pointer; cursor: pointer;
} }
.ClientListView ul { .ClientListView .list {
list-style: none; padding: 16px 0;
padding: 0;
} }
.ListedClientView .header { .ClientView {
border: 1px solid #E6E6E6;
border-radius: 8px;
margin: 16px 0;
padding: 16px;
}
.ClientView .header {
display: flex; display: flex;
} }
.ListedClientView .description { .ClientView .description {
flex: 1; flex: 1;
} }
.ListedClientView h3 { .ClientView h3 {
margin-top: 0; margin-top: 0;
} }
.ListedClientView .icon { .ClientView .icon {
border-radius: 8px; border-radius: 8px;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
@ -164,8 +165,7 @@ button.text:hover {
height: 60px; height: 60px;
} }
.ClientView .icon.element-io {
.ListedClientView .icon.element-io {
background-image: url('../images/client-icons/element.svg'); background-image: url('../images/client-icons/element.svg');
} }
@ -195,12 +195,13 @@ button.primary, a.primary {
button.primary, button.secondary { button.primary, button.secondary {
border: none; border: none;
width: 100%;
font-size: inherit; font-size: inherit;
} }
.PreviewView .primary, .PreviewView .secondary { .fullwidth {
display: block; display: block;
width: 100%;
box-sizing: border-box;
} }
.ClientView .actions a.badge { .ClientView .actions a.badge {

View file

@ -15,14 +15,38 @@ limitations under the License.
*/ */
import {TemplateView} from "../utils/TemplateView.js"; import {TemplateView} from "../utils/TemplateView.js";
import {ListedClientView} from "./ClientView.js"; import {ClientView} from "./ClientView.js";
export class ClientListView extends TemplateView { export class ClientListView extends TemplateView {
render(t, vm) { render(t, vm) {
const clients = vm.clients.map(clientViewModel => t.view(new ListedClientView(clientViewModel))); return t.mapView(vm => vm.clientViewModel, () => {
if (vm.clientViewModel) {
return new ContinueWithClientView(vm);
} else {
return new AllClientsView(vm);
}
});
}
}
class AllClientsView extends TemplateView {
render(t, vm) {
const clients = vm.clientList.map(clientViewModel => t.view(new ClientView(clientViewModel)));
return t.div({className: "ClientListView"}, [ return t.div({className: "ClientListView"}, [
t.h3("You need an app to continue"), t.h2("Choose an app to continue"),
t.div({className: "ClientListView"}, clients) t.div({className: "list"}, clients)
]);
}
}
class ContinueWithClientView extends TemplateView {
render(t, vm) {
return t.div({className: "ClientListView"}, [
t.h2([
`Continue with ${vm.clientViewModel.name} `,
t.button({onClick: () => vm.showAll()}, "Back")
]),
t.div({className: "list"}, t.view(new ClientView(vm.clientViewModel)))
]); ]);
} }
} }

View file

@ -21,7 +21,25 @@ import {ViewModel} from "../utils/ViewModel.js";
export class ClientListViewModel extends ViewModel { export class ClientListViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
const {clients, link} = options; const {clients, client, link} = options;
this.clients = clients.map(client => new ClientViewModel(this.childOptions({client, link, showAcceptAnyway: true}))); this.clientList = clients.map(client => new ClientViewModel(this.childOptions({
client,
link,
pickClient: client => this._pickClient(client)
})));
this.clientViewModel = null;
if (client) {
this._pickClient(client);
}
}
_pickClient(client) {
this.clientViewModel = this.clientList.find(vm => vm.clientId === client.id);
this.emitChange();
}
showAll() {
this.clientViewModel = null;
this.emitChange();
} }
} }

View file

@ -16,9 +16,9 @@ limitations under the License.
import {TemplateView} from "../utils/TemplateView.js"; import {TemplateView} from "../utils/TemplateView.js";
export class ListedClientView extends TemplateView { export class ClientView extends TemplateView {
render(t, vm) { render(t, vm) {
return t.div({className: "ListedClientView"}, [ return t.div({className: "ClientView"}, [
t.div({className: "header"}, [ t.div({className: "header"}, [
t.div({className: "description"}, [ t.div({className: "description"}, [
t.h3(vm.name), t.h3(vm.name),
@ -26,14 +26,6 @@ export class ListedClientView extends TemplateView {
]), ]),
t.div({className: `icon ${vm.clientId}`}) t.div({className: `icon ${vm.clientId}`})
]), ]),
t.view(new ClientView(vm))
]);
}
}
export class ClientView extends TemplateView {
render(t, vm) {
return t.div({className: "ClientView"}, [
t.mapView(vm => vm.stage, stage => { t.mapView(vm => vm.stage, stage => {
switch (stage) { switch (stage) {
case "open": return new OpenClientView(vm); case "open": return new OpenClientView(vm);
@ -48,38 +40,51 @@ class OpenClientView extends TemplateView {
render(t, vm) { render(t, vm) {
return t.div({className: "OpenClientView"}, [ return t.div({className: "OpenClientView"}, [
t.a({ t.a({
className: "primary", className: "primary fullwidth",
href: vm.deepLink, href: vm.deepLink,
rel: "noopener noreferrer", rel: "noopener noreferrer",
onClick: () => vm.deepLinkActivated(), onClick: () => vm.deepLinkActivated(),
}, vm.deepLinkLabel) }, "Continue")
]); ]);
} }
} }
class InstallClientView extends TemplateView { class InstallClientView extends TemplateView {
copyToClipboard() {
}
render(t, vm) { render(t, vm) {
return t.div({className: "InstallClientView"}, [ const children = [];
t.h3(`Looks like you don't have ${vm.name} installed.`),
t.div({className: "actions"}, vm.actions.map(a => { if (vm.textInstructions) {
let badgeUrl; const copy = t.button({className: "primary", onClick: evt => this.copyToClipboard(evt)}, "Copy");
switch (a.kind) { children.push(t.p([vm.textInstructions, copy]));
case "play-store": badgeUrl = "images/google-play-us.svg"; break; }
case "fdroid": badgeUrl = "images/fdroid-badge.png"; break;
case "apple-app-store": badgeUrl = "images/app-store-us-alt.svg"; break; const actions = t.div({className: "actions"}, vm.actions.map(a => {
} let badgeUrl;
return t.a({ switch (a.kind) {
href: a.url, case "play-store": badgeUrl = "images/google-play-us.svg"; break;
className: { case "fdroid": badgeUrl = "images/fdroid-badge.png"; break;
primary: a.primary && !badgeUrl, case "apple-app-store": badgeUrl = "images/app-store-us-alt.svg"; break;
secondary: !a.primary && !badgeUrl, }
badge: !!badgeUrl, return t.a({
}, href: a.url,
rel: "noopener noreferrer", className: {
["aria-label"]: a.label, fullwidth: true,
onClick: () => a.activated() primary: a.primary && !badgeUrl,
}, badgeUrl ? t.img({src: badgeUrl}) : a.label); secondary: !a.primary && !badgeUrl,
})) badge: !!badgeUrl,
]); },
rel: "noopener noreferrer",
["aria-label"]: a.label,
onClick: () => a.activated()
}, badgeUrl ? t.img({src: badgeUrl}) : a.label);
}));
children.push(actions);
return t.div({className: "InstallClientView"}, children);
} }
} }

View file

@ -16,28 +16,28 @@ limitations under the License.
import {isWebPlatform, Platform} from "./Platform.js"; import {isWebPlatform, Platform} from "./Platform.js";
import {ViewModel} from "../utils/ViewModel.js"; import {ViewModel} from "../utils/ViewModel.js";
import {getLabelForLinkKind} from "../Link.js";
export class ClientViewModel extends ViewModel { export class ClientViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
const {client, link} = options; const {client, link, pickClient} = options;
this._client = client; this._client = client;
this._link = link; this._link = link;
this._pickClient = pickClient;
const supportedPlatforms = client.platforms; const supportedPlatforms = client.platforms;
const matchingPlatforms = this.platforms.filter(p => { const matchingPlatforms = this.platforms.filter(p => {
return supportedPlatforms.includes(p); return supportedPlatforms.includes(p);
}); });
const nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p)); const nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p));
const webPlatform = this.platforms.find(p => isWebPlatform(p)); const webPlatform = matchingPlatforms.find(p => isWebPlatform(p));
this._showOpen = nativePlatform && !client.canInterceptMatrixToLinks(nativePlatform);
this._proposedPlatform = this.preferences.platform || nativePlatform || webPlatform; this._proposedPlatform = this.preferences.platform || nativePlatform || webPlatform;
this.actions = this._createActions(client, link, nativePlatform, webPlatform); this.actions = this._createActions(client, link, nativePlatform, webPlatform);
this.name = this._client.getName(this._proposedPlatform); this.name = this._client.getName(this._proposedPlatform);
this.deepLink = this._client.getDeepLink(this._proposedPlatform, this._link); this.deepLink = this._client.getDeepLink(this._proposedPlatform, this._link);
this._showOpen = this.deepLink && nativePlatform && !client.canInterceptMatrixToLinks(nativePlatform);
} }
_createActions(client, link, nativePlatform, webPlatform) { _createActions(client, link, nativePlatform, webPlatform) {
@ -65,6 +65,10 @@ export class ClientViewModel extends ViewModel {
return actions; return actions;
} }
get identifier() {
return this._link.identifier;
}
get description() { get description() {
return this._client.description; return this._client.description;
} }
@ -77,11 +81,12 @@ export class ClientViewModel extends ViewModel {
return this._showOpen ? "open" : "install"; return this._showOpen ? "open" : "install";
} }
get deepLinkLabel() { get textInstructions() {
return getLabelForLinkKind(this._link.kind); return this._client.getLinkInstructions(this._proposedPlatform, this._link);
} }
deepLinkActivated() { deepLinkActivated() {
this._pickClient(this._client);
this.preferences.setClient(this._client.id, this._proposedPlatform); this.preferences.setClient(this._client.id, this._proposedPlatform);
if (this._showOpen) { if (this._showOpen) {
this._showOpen = false; this._showOpen = false;

View file

@ -32,12 +32,10 @@ export class Element {
]; ];
} }
get description() { return 'Fully-featured Matrix client'; } get description() { return 'Fully-featured Matrix client, used by millions.'; }
getMaturity(platform) { return Maturity.Stable; } getMaturity(platform) { return Maturity.Stable; }
getLinkSupport(platform, link) { return true; }
getDeepLink(platform, link) { getDeepLink(platform, link) {
let fragmentPath; let fragmentPath;
switch (link.kind) { switch (link.kind) {

View file

@ -21,23 +21,19 @@ import {ClientView} from "../client/ClientView.js";
export class PreviewView extends TemplateView { export class PreviewView extends TemplateView {
render(t, vm) { render(t, vm) {
return t.div({className: "PreviewView card"}, [ return t.div({className: "PreviewView card"}, [
t.h2({className: {hidden: vm => !vm.loading}}, "Loading preview…"), t.h1({className: {hidden: vm => !vm.loading}}, "Loading preview…"),
t.div({className: {hidden: vm => vm.loading}}, [ t.div({className: {hidden: vm => vm.loading}}, [
t.div({className: "preview"}, [ t.div({className: "preview"}, [
t.p(t.img({className: "avatar", src: vm => vm.avatarUrl})), t.p(t.img({className: "avatar", src: vm => vm.avatarUrl})),
t.div({className: "profileInfo"}, [ t.h1(vm => vm.name),
t.h2(vm => vm.name), t.p(vm => vm.identifier),
t.p(vm => vm.identifier),
t.p(["Preview from ", vm => vm.previewDomain]),
]),
]), ]),
t.p({className: {hidden: vm => !vm.canShowClients}}, t.button({ t.p({className: {hidden: vm => vm.clientsViewModel}}, t.button({
className: "primary", className: "primary fullwidth",
onClick: () => vm.showClients() onClick: () => vm.showClients()
}, vm => vm.showClientsLabel)), }, vm => vm.showClientsLabel)),
t.mapView(vm => vm.clientsViewModel, childVM => childVM ? new ClientListView(childVM) : null), t.mapView(vm => vm.clientsViewModel, childVM => childVM ? new ClientListView(childVM) : null),
t.mapView(vm => vm.preferredClientViewModel, childVM => childVM ? new ClientView(childVM) : null), t.p(["Preview provided by ", vm => vm.previewDomain]),
t.p({className: {hidden: vm => !vm.preferredClientViewModel}}, vm => `This will open in ${vm.preferredClientViewModel?.name}`),
]) ])
]); ]);
} }

View file

@ -35,9 +35,10 @@ export class PreviewViewModel extends ViewModel {
this.previewDomain = null; this.previewDomain = null;
this.clientsViewModel = null; this.clientsViewModel = null;
this.acceptInstructions = null; this.acceptInstructions = null;
this.preferredClientViewModel = this._preferredClient ? new ClientViewModel(this.childOptions({ this.clientsViewModel = this._preferredClient ? new ClientListViewModel(this.childOptions({
clients: this._clients,
client: this._preferredClient, client: this._preferredClient,
link: this._link link: this._link,
})) : null; })) : null;
} }
@ -74,16 +75,12 @@ export class PreviewViewModel extends ViewModel {
return this._link.identifier; return this._link.identifier;
} }
get canShowClients() {
return !(this.preferredClientViewModel || this.clientsViewModel);
}
get showClientsLabel() { get showClientsLabel() {
return getLabelForLinkKind(this._link.kind); return getLabelForLinkKind(this._link.kind);
} }
showClients() { showClients() {
if (!this._preferredClient) { if (!this.clientsViewModel) {
this.clientsViewModel = new ClientListViewModel(this.childOptions({clients: this._clients, link: this._link})); this.clientsViewModel = new ClientListViewModel(this.childOptions({clients: this._clients, link: this._link}));
this.emitChange(); this.emitChange();
} }