Merge pull request #202 from matrix-org/bwindels/fix-201

Show both open and install options when deeplink for native app is https link
This commit is contained in:
Bruno Windels 2021-04-12 15:25:13 +02:00 committed by GitHub
commit 356ad93a2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 71 additions and 37 deletions

View file

@ -33,12 +33,10 @@ class AllClientsView extends TemplateView {
render(t, vm) { render(t, vm) {
return t.div({className: "ClientListView"}, [ return t.div({className: "ClientListView"}, [
t.h2("Choose an app to continue"), t.h2("Choose an app to continue"),
t.mapView(vm => vm.clientList, () => { t.map(vm => vm.clientList, (clientList, t) => {
return new TemplateView(vm, t => { return t.div({className: "list"}, clientList.map(clientViewModel => {
return t.div({className: "list"}, vm.clientList.map(clientViewModel => {
return t.view(new ClientView(clientViewModel)); return t.view(new ClientView(clientViewModel));
})); }));
});
}), }),
t.div(t.label([ t.div(t.label([
t.input({ t.input({

View file

@ -53,12 +53,8 @@ export class ClientView extends TemplateView {
]), ]),
t.img({className: "clientIcon", src: vm.iconUrl}) t.img({className: "clientIcon", src: vm.iconUrl})
]), ]),
t.mapView(vm => vm.stage, stage => { t.ifView(vm => vm.showOpen, vm => new OpenClientView(vm)),
switch (stage) { t.ifView(vm => vm.showInstall, vm => new InstallClientView(vm))
case "open": return new OpenClientView(vm);
case "install": return new InstallClientView(vm);
}
}),
]); ]);
} }
} }

View file

@ -43,12 +43,21 @@ export class ClientViewModel extends ViewModel {
this._webPlatform = matchingPlatforms.find(p => isWebPlatform(p)); this._webPlatform = matchingPlatforms.find(p => isWebPlatform(p));
this._nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p)); this._nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p));
const preferredPlatform = matchingPlatforms.find(p => p === this.preferences.platform); const preferredPlatform = matchingPlatforms.find(p => p === this.preferences.platform);
this._proposedPlatform = preferredPlatform || this._nativePlatform || webPlatform; this._proposedPlatform = preferredPlatform || this._nativePlatform || this._webPlatform;
this.openActions = this._createOpenActions(); this.openActions = this._createOpenActions();
this.installActions = this._createInstallActions(); this.installActions = this._createInstallActions();
this._clientCanIntercept = !!(this._nativePlatform && this._client.canInterceptMatrixToLinks(this._nativePlatform)); this._clientCanIntercept = !!(this._nativePlatform && this._client.canInterceptMatrixToLinks(this._nativePlatform));
this._showOpen = this.openActions.length && !this._clientCanIntercept; this._showOpen = this.openActions.length && !this._clientCanIntercept;
const proposedDeepLink = this._client.getDeepLink(this._proposedPlatform, this._link);
this._openWillNavigateIfNotInstalled = false;
if (this._showOpen && !isWebPlatform(this._proposedPlatform)) {
try {
if (new URL(proposedDeepLink).protocol === "https:") {
this._openWillNavigateIfNotInstalled = true;
}
} catch (err) {}
}
} }
// these are only shown in the open stage // these are only shown in the open stage
@ -170,8 +179,16 @@ export class ClientViewModel extends ViewModel {
return this._client.icon; return this._client.icon;
} }
get stage() { get showOpen() {
return this._showOpen ? "open" : "install"; return this._showOpen;
}
get showInstall() {
// also show install options in open screen if the deeplink is
// a https link that should be intercepted by the native app
// because if it isn't installed, you will just go to that
// website and never see the install options here.
return !this._showOpen || this._openWillNavigateIfNotInstalled;
} }
get textInstructions() { get textInstructions() {

View file

@ -44,11 +44,11 @@ class LoadingPreviewView extends TemplateView {
class LoadedPreviewView extends TemplateView { class LoadedPreviewView extends TemplateView {
render(t, vm) { render(t, vm) {
const avatar = t.mapView(vm => vm.avatarUrl, avatarUrl => { const avatar = t.map(vm => vm.avatarUrl, (avatarUrl, t) => {
if (avatarUrl) { if (avatarUrl) {
return new TemplateView(avatarUrl, (t, src) => t.img({className: "avatar", src})); return t.img({className: "avatar", src: avatarUrl});
} else { } else {
return new TemplateView(null, t => t.div({className: "defaultAvatar"})); return t.div({className: "defaultAvatar"});
} }
}); });
return t.div([ return t.div([

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS, tag } from "./html.js"; import { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS } from "./html.js";
/** /**
Bindable template. Renders once, and allows bindings for given nodes. If you need Bindable template. Renders once, and allows bindings for given nodes. If you need
@ -33,11 +33,13 @@ import { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS, tag } f
export class TemplateView { export class TemplateView {
constructor(value, render = undefined) { constructor(value, render = undefined) {
this._value = value; this._value = value;
// TODO: can avoid this if we have a separate class for inline templates vs class template views
this._render = render; this._render = render;
this._eventListeners = null; this._eventListeners = null;
this._bindings = null; this._bindings = null;
this._subViews = null; this._subViews = null;
this._root = null; this._root = null;
// TODO: can avoid this if we adopt the handleEvent pattern in our EventListener
this._boundUpdateFromValue = null; this._boundUpdateFromValue = null;
} }
@ -108,6 +110,10 @@ export class TemplateView {
return this._root; return this._root;
} }
_updateFromValue(changedProps) {
this.update(this._value, changedProps);
}
update(value) { update(value) {
this._value = value; this._value = value;
if (this._bindings) { if (this._bindings) {
@ -117,10 +123,6 @@ export class TemplateView {
} }
} }
_updateFromValue(changedProps) {
this.update(this._value, changedProps);
}
_addEventListener(node, name, fn, useCapture = false) { _addEventListener(node, name, fn, useCapture = false) {
if (!this._eventListeners) { if (!this._eventListeners) {
this._eventListeners = []; this._eventListeners = [];
@ -135,12 +137,19 @@ export class TemplateView {
this._bindings.push(bindingFn); this._bindings.push(bindingFn);
} }
_addSubView(view) { addSubView(view) {
if (!this._subViews) { if (!this._subViews) {
this._subViews = []; this._subViews = [];
} }
this._subViews.push(view); this._subViews.push(view);
} }
removeSubView(view) {
const idx = this._subViews.indexOf(view);
if (idx !== -1) {
this._subViews.splice(idx, 1);
}
}
} }
// what is passed to render // what is passed to render
@ -238,8 +247,6 @@ class TemplateBuilder {
const newNode = renderNode(node); const newNode = renderNode(node);
if (node.parentNode) { if (node.parentNode) {
node.parentNode.replaceChild(newNode, node); node.parentNode.replaceChild(newNode, node);
} else {
console.warn("Could not update parent of node binding");
} }
node = newNode; node = newNode;
} }
@ -279,15 +286,10 @@ class TemplateBuilder {
} catch (err) { } catch (err) {
return errorToDOM(err); return errorToDOM(err);
} }
this._templateView._addSubView(view); this._templateView.addSubView(view);
return root; return root;
} }
// sugar
createTemplate(render) {
return vm => new TemplateView(vm, render);
}
// map a value to a view, every time the value changes // map a value to a view, every time the value changes
mapView(mapFn, viewCreator) { mapView(mapFn, viewCreator) {
return this._addReplaceNodeBinding(mapFn, (prevNode) => { return this._addReplaceNodeBinding(mapFn, (prevNode) => {
@ -308,15 +310,36 @@ class TemplateBuilder {
}); });
} }
// creates a conditional subtemplate // Special case of mapView for a TemplateView.
if(fn, viewCreator) { // Always creates a TemplateView, if this is optional depending
// on mappedValue, use `if` or `mapView`
map(mapFn, renderFn) {
return this.mapView(mapFn, mappedValue => {
return new TemplateView(this._value, (t, vm) => {
const rootNode = renderFn(mappedValue, t, vm);
if (!rootNode) {
// TODO: this will confuse mapView which assumes that
// a comment node means there is no view to clean up
return document.createComment("map placeholder");
}
return rootNode;
});
});
}
ifView(predicate, viewCreator) {
return this.mapView( return this.mapView(
value => !!fn(value), value => !!predicate(value),
enabled => enabled ? viewCreator(this._value) : null enabled => enabled ? viewCreator(this._value) : null
); );
} }
}
// creates a conditional subtemplate
// use mapView if you need to map to a different view class
if(predicate, renderFn) {
return this.ifView(predicate, vm => new TemplateView(vm, renderFn));
}
}
function errorToDOM(error) { function errorToDOM(error) {
const stack = new Error().stack; const stack = new Error().stack;