Mass Tabs -> Spaces conversion

This commit is contained in:
Michael Telatynski
2021-07-23 12:25:21 +01:00
parent 9caf2ce268
commit acd881385c
31 changed files with 789 additions and 789 deletions
+99 -99
View File
@@ -24,20 +24,20 @@ const EVENTID_PATTERN = /^$([^:]+):(.+)$/;
const GROUPID_PATTERN = /^\+([^:]+):(.+)$/;
export const IdentifierKind = createEnum(
"RoomId",
"RoomAlias",
"UserId",
"GroupId",
"RoomId",
"RoomAlias",
"UserId",
"GroupId",
);
function asPrefix(identifierKind) {
switch (identifierKind) {
case IdentifierKind.RoomId: return "!";
case IdentifierKind.RoomAlias: return "#";
case IdentifierKind.GroupId: return "+";
case IdentifierKind.UserId: return "@";
default: throw new Error("invalid id kind " + identifierKind);
}
switch (identifierKind) {
case IdentifierKind.RoomId: return "!";
case IdentifierKind.RoomAlias: return "#";
case IdentifierKind.GroupId: return "+";
case IdentifierKind.UserId: return "@";
default: throw new Error("invalid id kind " + identifierKind);
}
}
function getWebInstanceMap(queryParams) {
@@ -56,19 +56,19 @@ function getWebInstanceMap(queryParams) {
}
export function getLabelForLinkKind(kind) {
switch (kind) {
case LinkKind.User: return "Start chat";
case LinkKind.Room: return "View room";
case LinkKind.Group: return "View community";
case LinkKind.Event: return "View message";
}
switch (kind) {
case LinkKind.User: return "Start chat";
case LinkKind.Room: return "View room";
case LinkKind.Group: return "View community";
case LinkKind.Event: return "View message";
}
}
export const LinkKind = createEnum(
"Room",
"User",
"Group",
"Event"
"Room",
"User",
"Group",
"Event"
)
export class Link {
@@ -81,106 +81,106 @@ export class Link {
);
}
static parse(fragment) {
if (!fragment) {
return null;
}
let [linkStr, queryParamsStr] = fragment.split("?");
static parse(fragment) {
if (!fragment) {
return null;
}
let [linkStr, queryParamsStr] = fragment.split("?");
let viaServers = [];
let viaServers = [];
let clientId = null;
let webInstances = {};
if (queryParamsStr) {
if (queryParamsStr) {
const queryParams = queryParamsStr.split("&").map(pair => {
const [key, value] = pair.split("=");
return [decodeURIComponent(key), decodeURIComponent(value)];
});
viaServers = queryParams
.filter(([key, value]) => key === "via")
.map(([,value]) => value);
viaServers = queryParams
.filter(([key, value]) => key === "via")
.map(([,value]) => value);
const clientParam = queryParams.find(([key]) => key === "client");
if (clientParam) {
clientId = clientParam[1];
}
webInstances = getWebInstanceMap(queryParams);
}
}
if (linkStr.startsWith("#/")) {
linkStr = linkStr.substr(2);
}
if (linkStr.startsWith("#/")) {
linkStr = linkStr.substr(2);
}
const [identifier, eventId] = linkStr.split("/");
let matches;
matches = USERID_PATTERN.exec(identifier);
if (matches) {
const server = matches[2];
const localPart = matches[1];
return new Link(clientId, viaServers, IdentifierKind.UserId, localPart, server, webInstances);
}
matches = ROOMALIAS_PATTERN.exec(identifier);
if (matches) {
const server = matches[2];
const localPart = matches[1];
return new Link(clientId, viaServers, IdentifierKind.RoomAlias, localPart, server, webInstances, eventId);
}
matches = ROOMID_PATTERN.exec(identifier);
if (matches) {
const server = matches[2];
const localPart = matches[1];
return new Link(clientId, viaServers, IdentifierKind.RoomId, localPart, server, webInstances, eventId);
}
matches = GROUPID_PATTERN.exec(identifier);
if (matches) {
const server = matches[2];
const localPart = matches[1];
return new Link(clientId, viaServers, IdentifierKind.GroupId, localPart, server, webInstances);
}
return null;
}
let matches;
matches = USERID_PATTERN.exec(identifier);
if (matches) {
const server = matches[2];
const localPart = matches[1];
return new Link(clientId, viaServers, IdentifierKind.UserId, localPart, server, webInstances);
}
matches = ROOMALIAS_PATTERN.exec(identifier);
if (matches) {
const server = matches[2];
const localPart = matches[1];
return new Link(clientId, viaServers, IdentifierKind.RoomAlias, localPart, server, webInstances, eventId);
}
matches = ROOMID_PATTERN.exec(identifier);
if (matches) {
const server = matches[2];
const localPart = matches[1];
return new Link(clientId, viaServers, IdentifierKind.RoomId, localPart, server, webInstances, eventId);
}
matches = GROUPID_PATTERN.exec(identifier);
if (matches) {
const server = matches[2];
const localPart = matches[1];
return new Link(clientId, viaServers, IdentifierKind.GroupId, localPart, server, webInstances);
}
return null;
}
constructor(clientId, viaServers, identifierKind, localPart, server, webInstances, eventId) {
const servers = [server];
servers.push(...viaServers);
constructor(clientId, viaServers, identifierKind, localPart, server, webInstances, eventId) {
const servers = [server];
servers.push(...viaServers);
this.webInstances = webInstances;
this.servers = orderedUnique(servers);
this.identifierKind = identifierKind;
this.identifier = `${asPrefix(identifierKind)}${localPart}:${server}`;
this.eventId = eventId;
this.servers = orderedUnique(servers);
this.identifierKind = identifierKind;
this.identifier = `${asPrefix(identifierKind)}${localPart}:${server}`;
this.eventId = eventId;
this.clientId = clientId;
}
}
get kind() {
if (this.eventId) {
return LinkKind.Event;
}
switch (this.identifierKind) {
case IdentifierKind.RoomId:
case IdentifierKind.RoomAlias:
return LinkKind.Room;
case IdentifierKind.UserId:
return LinkKind.User;
case IdentifierKind.GroupId:
return LinkKind.Group;
default:
return null;
}
}
get kind() {
if (this.eventId) {
return LinkKind.Event;
}
switch (this.identifierKind) {
case IdentifierKind.RoomId:
case IdentifierKind.RoomAlias:
return LinkKind.Room;
case IdentifierKind.UserId:
return LinkKind.User;
case IdentifierKind.GroupId:
return LinkKind.Group;
default:
return null;
}
}
equals(link) {
return link &&
link.identifier === this.identifier &&
this.servers.length === link.servers.length &&
this.servers.every((s, i) => link.servers[i] === s) &&
equals(link) {
return link &&
link.identifier === this.identifier &&
this.servers.length === link.servers.length &&
this.servers.every((s, i) => link.servers[i] === s) &&
Object.keys(this.webInstances).length === Object.keys(link.webInstances).length &&
Object.keys(this.webInstances).every(k => this.webInstances[k] === link.webInstances[k]);
}
}
toFragment() {
if (this.eventId) {
return `/${this.identifier}/${this.eventId}`;
} else {
return `/${this.identifier}`;
}
}
toFragment() {
if (this.eventId) {
return `/${this.identifier}/${this.eventId}`;
} else {
return `/${this.identifier}`;
}
}
}
+10 -10
View File
@@ -17,17 +17,17 @@ limitations under the License.
import {createEnum} from "./utils/enum.js";
export const Platform = createEnum(
"DesktopWeb",
"MobileWeb",
"Android",
"iOS",
"Windows",
"macOS",
"Linux"
"DesktopWeb",
"MobileWeb",
"Android",
"iOS",
"Windows",
"macOS",
"Linux"
);
export function guessApplicablePlatforms(userAgent, platform) {
// return [Platform.DesktopWeb, Platform.Linux];
// return [Platform.DesktopWeb, Platform.Linux];
let nativePlatform;
let webPlatform;
if (/android/i.test(userAgent)) {
@@ -55,10 +55,10 @@ export function guessApplicablePlatforms(userAgent, platform) {
}
export function isWebPlatform(p) {
return p === Platform.DesktopWeb || p === Platform.MobileWeb;
return p === Platform.DesktopWeb || p === Platform.MobileWeb;
}
export function isDesktopPlatform(p) {
return p === Platform.Linux || p === Platform.Windows || p === Platform.macOS;
return p === Platform.Linux || p === Platform.Windows || p === Platform.macOS;
}
+29 -29
View File
@@ -18,51 +18,51 @@ import {Platform} from "./Platform.js";
import {EventEmitter} from "./utils/ViewModel.js";
export class Preferences extends EventEmitter {
constructor(localStorage) {
constructor(localStorage) {
super();
this._localStorage = localStorage;
this.clientId = null;
// used to differentiate web from native if a client supports both
this.platform = null;
this.homeservers = null;
this._localStorage = localStorage;
this.clientId = null;
// used to differentiate web from native if a client supports both
this.platform = null;
this.homeservers = null;
const prefsStr = localStorage.getItem("preferred_client");
if (prefsStr) {
const {id, platform} = JSON.parse(prefsStr);
this.clientId = id;
this.platform = Platform[platform];
}
const prefsStr = localStorage.getItem("preferred_client");
if (prefsStr) {
const {id, platform} = JSON.parse(prefsStr);
this.clientId = id;
this.platform = Platform[platform];
}
const serversStr = localStorage.getItem("consented_servers");
if (serversStr) {
this.homeservers = JSON.parse(serversStr);
}
}
}
setClient(id, platform) {
this.clientId = id;
platform = Platform[platform];
this.platform = platform;
this._localStorage.setItem("preferred_client", JSON.stringify({id, platform}));
setClient(id, platform) {
this.clientId = id;
platform = Platform[platform];
this.platform = platform;
this._localStorage.setItem("preferred_client", JSON.stringify({id, platform}));
this.emit("canClear")
}
}
setHomeservers(homeservers, persist) {
setHomeservers(homeservers, persist) {
this.homeservers = homeservers;
if (persist) {
this._localStorage.setItem("consented_servers", JSON.stringify(homeservers));
this.emit("canClear");
}
}
}
clear() {
this._localStorage.removeItem("preferred_client");
clear() {
this._localStorage.removeItem("preferred_client");
this._localStorage.removeItem("consented_servers");
this.clientId = null;
this.platform = null;
this.clientId = null;
this.platform = null;
this.homeservers = null;
}
}
get canClear() {
return !!this.clientId || !!this.platform || !!this.homeservers;
}
get canClear() {
return !!this.clientId || !!this.platform || !!this.homeservers;
}
}
+17 -17
View File
@@ -20,25 +20,25 @@ import {CreateLinkView} from "./create/CreateLinkView.js";
import {LoadServerPolicyView} from "./policy/LoadServerPolicyView.js";
export class RootView extends TemplateView {
render(t, vm) {
return t.div({className: "RootView"}, [
t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null),
t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null),
render(t, vm) {
return t.div({className: "RootView"}, [
t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null),
t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null),
t.mapView(vm => vm.loadServerPolicyViewModel, vm => vm ? new LoadServerPolicyView(vm) : null),
t.div({className: "footer"}, [
t.p(t.img({src: "images/matrix-logo.svg"})),
t.p(["This invite uses ", externalLink(t, "https://matrix.org", "Matrix"), ", an open network for secure, decentralized communication."]),
t.ul({className: "links"}, [
t.li(externalLink(t, "https://github.com/matrix-org/matrix.to", "GitHub project")),
t.li(externalLink(t, "https://github.com/matrix-org/matrix.to/tree/main/src/open/clients", "Add your app")),
t.li({className: {hidden: vm => !vm.hasPreferences}},
t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")),
])
])
]);
}
t.div({className: "footer"}, [
t.p(t.img({src: "images/matrix-logo.svg"})),
t.p(["This invite uses ", externalLink(t, "https://matrix.org", "Matrix"), ", an open network for secure, decentralized communication."]),
t.ul({className: "links"}, [
t.li(externalLink(t, "https://github.com/matrix-org/matrix.to", "GitHub project")),
t.li(externalLink(t, "https://github.com/matrix-org/matrix.to/tree/main/src/open/clients", "Add your app")),
t.li({className: {hidden: vm => !vm.hasPreferences}},
t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")),
])
])
]);
}
}
function externalLink(t, href, label) {
return t.a({href, target: "_blank", rel: "noopener noreferrer"}, label);
return t.a({href, target: "_blank", rel: "noopener noreferrer"}, label);
}
+33 -33
View File
@@ -23,51 +23,51 @@ import {LoadServerPolicyViewModel} from "./policy/LoadServerPolicyViewModel.js";
import {Platform} from "./Platform.js";
export class RootViewModel extends ViewModel {
constructor(options) {
super(options);
this.link = null;
this.openLinkViewModel = null;
this.createLinkViewModel = null;
constructor(options) {
super(options);
this.link = null;
this.openLinkViewModel = null;
this.createLinkViewModel = null;
this.loadServerPolicyViewModel = null;
this.preferences.on("canClear", () => {
this.emitChange();
});
}
}
_updateChildVMs(oldLink) {
if (this.link) {
this.createLinkViewModel = null;
if (!oldLink || !oldLink.equals(this.link)) {
this.openLinkViewModel = new OpenLinkViewModel(this.childOptions({
link: this.link,
clients: createClients(),
}));
}
} else {
this.openLinkViewModel = null;
this.createLinkViewModel = new CreateLinkViewModel(this.childOptions());
}
this.emitChange();
}
_updateChildVMs(oldLink) {
if (this.link) {
this.createLinkViewModel = null;
if (!oldLink || !oldLink.equals(this.link)) {
this.openLinkViewModel = new OpenLinkViewModel(this.childOptions({
link: this.link,
clients: createClients(),
}));
}
} else {
this.openLinkViewModel = null;
this.createLinkViewModel = new CreateLinkViewModel(this.childOptions());
}
this.emitChange();
}
updateHash(hash) {
updateHash(hash) {
if (hash.startsWith("#/policy/")) {
const server = hash.substr(9);
this.loadServerPolicyViewModel = new LoadServerPolicyViewModel(this.childOptions({server}));
this.loadServerPolicyViewModel.load();
} else {
const oldLink = this.link;
this.link = Link.parse(hash);
this._updateChildVMs(oldLink);
const oldLink = this.link;
this.link = Link.parse(hash);
this._updateChildVMs(oldLink);
}
}
}
clearPreferences() {
this.preferences.clear();
this._updateChildVMs();
}
clearPreferences() {
this.preferences.clear();
this._updateChildVMs();
}
get hasPreferences() {
return this.preferences.canClear;
}
get hasPreferences() {
return this.preferences.canClear;
}
}
+20 -20
View File
@@ -19,31 +19,31 @@ import {PreviewView} from "../preview/PreviewView.js";
import {copyButton} from "../utils/copy.js";
export class CreateLinkView extends TemplateView {
render(t, vm) {
render(t, vm) {
const link = t.a({href: vm => vm.linkUrl}, vm => vm.linkUrl);
return t.div({className: "CreateLinkView card"}, [
t.h1("Create shareable links to Matrix rooms, users or messages without being tied to any app"),
t.form({action: "#", onSubmit: evt => this._onSubmit(evt)}, [
t.div(t.input({
className: "fullwidth large",
type: "text",
name: "identifier",
return t.div({className: "CreateLinkView card"}, [
t.h1("Create shareable links to Matrix rooms, users or messages without being tied to any app"),
t.form({action: "#", onSubmit: evt => this._onSubmit(evt)}, [
t.div(t.input({
className: "fullwidth large",
type: "text",
name: "identifier",
required: true,
placeholder: "#room:example.com, @user:example.com",
placeholder: "#room:example.com, @user:example.com",
onChange: evt => this._onIdentifierChange(evt)
})),
t.div(t.input({className: "primary fullwidth icon link", type: "submit", value: "Create link"}))
]),
]);
}
})),
t.div(t.input({className: "primary fullwidth icon link", type: "submit", value: "Create link"}))
]),
]);
}
_onSubmit(evt) {
evt.preventDefault();
const form = evt.target;
const {identifier} = form.elements;
this.value.createLink(identifier.value);
_onSubmit(evt) {
evt.preventDefault();
const form = evt.target;
const {identifier} = form.elements;
this.value.createLink(identifier.value);
identifier.value = "";
}
}
_onIdentifierChange(evt) {
const inputField = evt.target;
+4 -4
View File
@@ -19,11 +19,11 @@ import {PreviewViewModel} from "../preview/PreviewViewModel.js";
import {Link} from "../Link.js";
export class CreateLinkViewModel extends ViewModel {
constructor(options) {
super(options);
constructor(options) {
super(options);
this._link = null;
this.previewViewModel = null;
}
this.previewViewModel = null;
}
validateIdentifier(identifier) {
return Link.validateIdentifier(identifier);
+14 -14
View File
@@ -21,18 +21,18 @@ import {Preferences} from "./Preferences.js";
import {guessApplicablePlatforms} from "./Platform.js";
export async function main(container) {
const vm = new RootViewModel({
request: xhrRequest,
openLink: url => location.href = url,
platforms: guessApplicablePlatforms(navigator.userAgent, navigator.platform),
preferences: new Preferences(window.localStorage),
origin: location.origin,
});
vm.updateHash(decodeURIComponent(location.hash));
window.__rootvm = vm;
const view = new RootView(vm);
container.appendChild(view.mount());
window.addEventListener('hashchange', () => {
vm.updateHash(decodeURIComponent(location.hash));
});
const vm = new RootViewModel({
request: xhrRequest,
openLink: url => location.href = url,
platforms: guessApplicablePlatforms(navigator.userAgent, navigator.platform),
preferences: new Preferences(window.localStorage),
origin: location.origin,
});
vm.updateHash(decodeURIComponent(location.hash));
window.__rootvm = vm;
const view = new RootView(vm);
container.appendChild(view.mount());
window.addEventListener('hashchange', () => {
vm.updateHash(decodeURIComponent(location.hash));
});
}
+40 -40
View File
@@ -18,50 +18,50 @@ import {TemplateView} from "../utils/TemplateView.js";
import {ClientView} from "./ClientView.js";
export class ClientListView extends TemplateView {
render(t, vm) {
return t.mapView(vm => vm.clientViewModel, () => {
if (vm.clientViewModel) {
return new ContinueWithClientView(vm);
} else {
return new AllClientsView(vm);
}
});
}
render(t, vm) {
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) {
return t.div({className: "ClientListView"}, [
t.h2("Choose an app to continue"),
t.map(vm => vm.clientList, (clientList, t) => {
return t.div({className: "list"}, clientList.map(clientViewModel => {
return t.view(new ClientView(clientViewModel));
}));
}),
t.div(t.label([
t.input({
type: "checkbox",
checked: vm.showUnsupportedPlatforms,
onChange: evt => vm.showUnsupportedPlatforms = evt.target.checked,
}),
"Show apps not available on my platform"
])),
t.div(t.label({className: "filterOption"}, [
t.input({
type: "checkbox",
checked: vm.showExperimental,
onChange: evt => vm.showExperimental = evt.target.checked,
}),
"Show experimental apps"
])),
]);
}
render(t, vm) {
return t.div({className: "ClientListView"}, [
t.h2("Choose an app to continue"),
t.map(vm => vm.clientList, (clientList, t) => {
return t.div({className: "list"}, clientList.map(clientViewModel => {
return t.view(new ClientView(clientViewModel));
}));
}),
t.div(t.label([
t.input({
type: "checkbox",
checked: vm.showUnsupportedPlatforms,
onChange: evt => vm.showUnsupportedPlatforms = evt.target.checked,
}),
"Show apps not available on my platform"
])),
t.div(t.label({className: "filterOption"}, [
t.input({
type: "checkbox",
checked: vm.showExperimental,
onChange: evt => vm.showExperimental = evt.target.checked,
}),
"Show experimental apps"
])),
]);
}
}
class ContinueWithClientView extends TemplateView {
render(t, vm) {
return t.div({className: "ClientListView"}, [
t.div({className: "list"}, t.view(new ClientView(vm.clientViewModel)))
]);
}
render(t, vm) {
return t.div({className: "ClientListView"}, [
t.div({className: "list"}, t.view(new ClientView(vm.clientViewModel)))
]);
}
}
+54 -54
View File
@@ -20,70 +20,70 @@ import {ClientViewModel} from "./ClientViewModel.js";
import {ViewModel} from "../utils/ViewModel.js";
export class ClientListViewModel extends ViewModel {
constructor(options) {
super(options);
const {clients, client, link} = options;
this._clients = clients;
this._link = link;
this.clientList = null;
this._showExperimental = false;
this._showUnsupportedPlatforms = false;
this._filterClients();
this.clientViewModel = null;
if (client) {
this._pickClient(client);
}
}
constructor(options) {
super(options);
const {clients, client, link} = options;
this._clients = clients;
this._link = link;
this.clientList = null;
this._showExperimental = false;
this._showUnsupportedPlatforms = false;
this._filterClients();
this.clientViewModel = null;
if (client) {
this._pickClient(client);
}
}
get showUnsupportedPlatforms() {
return this._showUnsupportedPlatforms;
}
get showUnsupportedPlatforms() {
return this._showUnsupportedPlatforms;
}
get showExperimental() {
return this._showExperimental;
}
get showExperimental() {
return this._showExperimental;
}
set showUnsupportedPlatforms(enabled) {
this._showUnsupportedPlatforms = enabled;
this._filterClients();
}
set showUnsupportedPlatforms(enabled) {
this._showUnsupportedPlatforms = enabled;
this._filterClients();
}
set showExperimental(enabled) {
this._showExperimental = enabled;
this._filterClients();
}
set showExperimental(enabled) {
this._showExperimental = enabled;
this._filterClients();
}
_filterClients() {
const clientVMs = this._clients.filter(client => {
_filterClients() {
const clientVMs = this._clients.filter(client => {
const platformMaturities = this.platforms.map(p => client.getMaturity(p));
const isStable = platformMaturities.includes(Maturity.Stable) || platformMaturities.includes(Maturity.Beta);
const isSupported = client.platforms.some(p => this.platforms.includes(p));
if (!this._showExperimental && !isStable) {
return false;
}
if (!this._showUnsupportedPlatforms && !isSupported) {
return false;
}
return true;
}).map(client => new ClientViewModel(this.childOptions({
client,
link: this._link,
pickClient: client => this._pickClient(client)
})));
const isStable = platformMaturities.includes(Maturity.Stable) || platformMaturities.includes(Maturity.Beta);
const isSupported = client.platforms.some(p => this.platforms.includes(p));
if (!this._showExperimental && !isStable) {
return false;
}
if (!this._showUnsupportedPlatforms && !isSupported) {
return false;
}
return true;
}).map(client => new ClientViewModel(this.childOptions({
client,
link: this._link,
pickClient: client => this._pickClient(client)
})));
const preferredClientVMs = clientVMs.filter(c => c.hasPreferredWebInstance);
const otherClientVMs = clientVMs.filter(c => !c.hasPreferredWebInstance);
this.clientList = preferredClientVMs.concat(otherClientVMs);
this.emitChange();
}
this.emitChange();
}
_pickClient(client) {
this.clientViewModel = this.clientList.find(vm => vm.clientId === client.id);
_pickClient(client) {
this.clientViewModel = this.clientList.find(vm => vm.clientId === client.id);
this.clientViewModel.pick(this);
this.emitChange();
}
this.emitChange();
}
showAll() {
this.clientViewModel = null;
this.emitChange();
}
showAll() {
this.clientViewModel = null;
this.emitChange();
}
}
+39 -39
View File
@@ -19,11 +19,11 @@ import {copy} from "../utils/copy.js";
import {text, tag} from "../utils/html.js";
function formatPlatforms(platforms) {
return platforms.reduce((str, p, i, all) => {
const first = i === 0;
const last = i === all.length - 1;
return str + (first ? "" : last ? " & " : ", ") + p;
}, "");
return platforms.reduce((str, p, i, all) => {
const first = i === 0;
const last = i === all.length - 1;
return str + (first ? "" : last ? " & " : ", ") + p;
}, "");
}
function renderInstructions(parts) {
@@ -38,46 +38,46 @@ function renderInstructions(parts) {
export class ClientView extends TemplateView {
render(t, vm) {
return t.div({className: {"ClientView": true, "isPreferred": vm => vm.hasPreferredWebInstance}}, [
render(t, vm) {
return t.div({className: {"ClientView": true, "isPreferred": vm => vm.hasPreferredWebInstance}}, [
... vm.hasPreferredWebInstance ? [t.div({className: "hostedBanner"}, vm.hostedByBannerLabel)] : [],
t.div({className: "header"}, [
t.div({className: "description"}, [
t.h3(vm.name),
t.p([vm.description, " ", t.a({
t.div({className: "header"}, [
t.div({className: "description"}, [
t.h3(vm.name),
t.p([vm.description, " ", t.a({
href: vm.homepage,
target: "_blank",
rel: "noopener noreferrer"
}, "Learn more")]),
t.p({className: "platforms"}, formatPlatforms(vm.availableOnPlatformNames)),
]),
t.img({className: "clientIcon", src: vm.iconUrl})
]),
t.p({className: "platforms"}, formatPlatforms(vm.availableOnPlatformNames)),
]),
t.img({className: "clientIcon", src: vm.iconUrl})
]),
t.mapView(vm => vm.stage, stage => {
switch (stage) {
case "open": return new OpenClientView(vm);
case "install": return new InstallClientView(vm);
}
}),
]);
}
]);
}
}
class OpenClientView extends TemplateView {
render(t, vm) {
return t.div({className: "OpenClientView"}, [
...vm.openActions.map(a => renderAction(t, a)),
render(t, vm) {
return t.div({className: "OpenClientView"}, [
...vm.openActions.map(a => renderAction(t, a)),
showBack(t, vm),
]);
}
]);
}
}
class InstallClientView extends TemplateView {
render(t, vm) {
const children = [];
render(t, vm) {
const children = [];
const textInstructions = vm.textInstructions;
if (textInstructions) {
if (textInstructions) {
const copyButton = t.button({
className: "copy",
title: "Copy instructions",
@@ -91,25 +91,25 @@ class InstallClientView extends TemplateView {
}
}
});
children.push(t.p({className: "instructions"}, renderInstructions(textInstructions).concat(copyButton)));
}
children.push(t.p({className: "instructions"}, renderInstructions(textInstructions).concat(copyButton)));
}
const actions = t.div({className: "actions"}, vm.installActions.map(a => renderAction(t, a)));
children.push(actions);
const actions = t.div({className: "actions"}, vm.installActions.map(a => renderAction(t, a)));
children.push(actions);
if (vm.showDeepLinkInInstall) {
const openItHere = t.a({
rel: "noopener noreferrer",
href: vm.openActions[0].url,
onClick: () => vm.openActions[0].activated(),
}, "open it here");
children.push(t.p([`If you already have ${vm.name} installed, you can `, openItHere, "."]))
}
if (vm.showDeepLinkInInstall) {
const openItHere = t.a({
rel: "noopener noreferrer",
href: vm.openActions[0].url,
onClick: () => vm.openActions[0].activated(),
}, "open it here");
children.push(t.p([`If you already have ${vm.name} installed, you can `, openItHere, "."]))
}
children.push(showBack(t, vm));
return t.div({className: "InstallClientView"}, children);
}
return t.div({className: "InstallClientView"}, children);
}
}
function showBack(t, vm) {
+77 -77
View File
@@ -27,28 +27,28 @@ function getMatchingPlatforms(client, supportedPlatforms) {
}
export class ClientViewModel extends ViewModel {
constructor(options) {
super(options);
const {client, link, pickClient} = options;
this._client = client;
this._link = link;
this._pickClient = pickClient;
constructor(options) {
super(options);
const {client, link, pickClient} = options;
this._client = client;
this._link = link;
this._pickClient = pickClient;
// to provide "choose other client" button after calling pick()
this._clientListViewModel = null;
this._update();
}
}
_update() {
const matchingPlatforms = getMatchingPlatforms(this._client, this.platforms);
this._webPlatform = matchingPlatforms.find(p => isWebPlatform(p));
this._nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p));
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;
this._proposedPlatform = preferredPlatform || this._nativePlatform || this._webPlatform;
this.openActions = this._createOpenActions();
this.installActions = this._createInstallActions();
this._clientCanIntercept = !!(this._nativePlatform && this._client.canInterceptMatrixToLinks(this._nativePlatform));
this._showOpen = this.openActions.length && !this._clientCanIntercept;
this.installActions = this._createInstallActions();
this._clientCanIntercept = !!(this._nativePlatform && this._client.canInterceptMatrixToLinks(this._nativePlatform));
this._showOpen = this.openActions.length && !this._clientCanIntercept;
}
// these are only shown in the open stage
@@ -93,40 +93,40 @@ export class ClientViewModel extends ViewModel {
}
// these are only shown in the install stage
_createInstallActions() {
let actions = [];
if (this._nativePlatform) {
const nativeActions = (this._client.getInstallLinks(this._nativePlatform) || []).map(installLink => {
return {
label: installLink.getDescription(this._nativePlatform),
url: installLink.createInstallURL(this._link),
kind: installLink.channelId,
primary: true,
activated: () => this.preferences.setClient(this._client.id, this._nativePlatform),
};
});
actions.push(...nativeActions);
}
if (this._webPlatform) {
const webDeepLink = this._client.getDeepLink(this._webPlatform, this._link);
if (webDeepLink) {
_createInstallActions() {
let actions = [];
if (this._nativePlatform) {
const nativeActions = (this._client.getInstallLinks(this._nativePlatform) || []).map(installLink => {
return {
label: installLink.getDescription(this._nativePlatform),
url: installLink.createInstallURL(this._link),
kind: installLink.channelId,
primary: true,
activated: () => this.preferences.setClient(this._client.id, this._nativePlatform),
};
});
actions.push(...nativeActions);
}
if (this._webPlatform) {
const webDeepLink = this._client.getDeepLink(this._webPlatform, this._link);
if (webDeepLink) {
const webLabel = this.hasPreferredWebInstance ?
`Open on ${this._client.getPreferredWebInstance(this._link)}` :
`Continue in your browser`;
actions.push({
label: webLabel,
url: webDeepLink,
kind: "open-in-web",
activated: () => {
actions.push({
label: webLabel,
url: webDeepLink,
kind: "open-in-web",
activated: () => {
if (!this.hasPreferredWebInstance) {
this.preferences.setClient(this._client.id, this._webPlatform);
}
},
});
}
}
return actions;
}
});
}
}
return actions;
}
get hasPreferredWebInstance() {
// also check there is a web platform that matches the platforms the user is on (mobile or desktop web)
@@ -150,17 +150,17 @@ export class ClientViewModel extends ViewModel {
return this._client.homepage;
}
get identifier() {
return this._link.identifier;
}
get identifier() {
return this._link.identifier;
}
get description() {
return this._client.description;
}
get description() {
return this._client.description;
}
get clientId() {
return this._client.id;
}
get clientId() {
return this._client.id;
}
get name() {
return this._client.name;
@@ -174,44 +174,44 @@ export class ClientViewModel extends ViewModel {
return this._showOpen ? "open" : "install";
}
get textInstructions() {
get textInstructions() {
let instructions = this._client.getLinkInstructions(this._proposedPlatform, this._link);
if (instructions && !Array.isArray(instructions)) {
instructions = [instructions];
}
return instructions;
}
return instructions;
}
get copyString() {
return this._client.getCopyString(this._proposedPlatform, this._link);
}
get showDeepLinkInInstall() {
get showDeepLinkInInstall() {
// we can assume this._nativePlatform as this._clientCanIntercept already checks it
return this._clientCanIntercept && !!this._client.getDeepLink(this._nativePlatform, this._link);
}
return this._clientCanIntercept && !!this._client.getDeepLink(this._nativePlatform, this._link);
}
get availableOnPlatformNames() {
const platforms = this._client.platforms;
const textPlatforms = [];
const hasWebPlatform = platforms.some(p => isWebPlatform(p));
if (hasWebPlatform) {
textPlatforms.push("Web");
}
const desktopPlatforms = platforms.filter(p => isDesktopPlatform(p));
if (desktopPlatforms.length === 1) {
textPlatforms.push(desktopPlatforms[0]);
} else {
textPlatforms.push("Desktop");
}
if (platforms.includes(Platform.Android)) {
textPlatforms.push("Android");
}
if (platforms.includes(Platform.iOS)) {
textPlatforms.push("iOS");
}
return textPlatforms;
}
get availableOnPlatformNames() {
const platforms = this._client.platforms;
const textPlatforms = [];
const hasWebPlatform = platforms.some(p => isWebPlatform(p));
if (hasWebPlatform) {
textPlatforms.push("Web");
}
const desktopPlatforms = platforms.filter(p => isDesktopPlatform(p));
if (desktopPlatforms.length === 1) {
textPlatforms.push(desktopPlatforms[0]);
} else {
textPlatforms.push("Desktop");
}
if (platforms.includes(Platform.Android)) {
textPlatforms.push("Android");
}
if (platforms.includes(Platform.iOS)) {
textPlatforms.push("iOS");
}
return textPlatforms;
}
pick(clientListViewModel) {
this._clientListViewModel = clientListViewModel;
+5 -5
View File
@@ -20,14 +20,14 @@ import {PreviewView} from "../preview/PreviewView.js";
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 ?
render(t, vm) {
return t.div({className: "OpenLinkView card"}, [
t.mapView(vm => vm.previewViewModel, previewVM => previewVM ?
new ShowLinkView(vm) :
new ServerConsentView(vm.serverConsentViewModel)
),
]);
}
]);
}
}
class ShowLinkView extends TemplateView {
+19 -19
View File
@@ -23,21 +23,21 @@ import {getLabelForLinkKind} from "../Link.js";
import {orderedUnique} from "../utils/unique.js";
export class OpenLinkViewModel extends ViewModel {
constructor(options) {
super(options);
const {clients, link} = options;
this._link = link;
this._clients = clients;
constructor(options) {
super(options);
const {clients, link} = options;
this._link = link;
this._clients = clients;
this.serverConsentViewModel = null;
this.previewViewModel = null;
this.previewViewModel = null;
this.clientsViewModel = null;
this.previewLoading = false;
this.previewLoading = false;
if (this.preferences.homeservers === null) {
this._showServerConsent();
} else {
this._showLink();
}
}
}
_showServerConsent() {
let servers = [];
@@ -67,24 +67,24 @@ export class OpenLinkViewModel extends ViewModel {
link: this._link,
consentedServers: this.preferences.homeservers
}));
this.previewLoading = true;
this.emitChange();
await this.previewViewModel.load();
this.previewLoading = false;
this.emitChange();
this.previewLoading = true;
this.emitChange();
await this.previewViewModel.load();
this.previewLoading = false;
this.emitChange();
}
get previewDomain() {
return this.previewViewModel?.domain;
}
get previewDomain() {
return this.previewViewModel?.domain;
}
get previewFailed() {
return this.previewViewModel?.failed;
}
get showClientsLabel() {
return getLabelForLinkKind(this._link.kind);
}
get showClientsLabel() {
return getLabelForLinkKind(this._link.kind);
}
changeServer() {
this.previewViewModel = null;
+2 -2
View File
@@ -27,7 +27,7 @@ export class ServerConsentView extends TemplateView {
className: "text",
onClick: () => vm.continueWithoutConsent(this._askEveryTimeChecked)
}, "continue without a preview");
return t.div({className: "ServerConsentView"}, [
return t.div({className: "ServerConsentView"}, [
t.p([
"Preview this link using the ",
t.strong(vm => vm.selectedServer || "…"),
@@ -56,7 +56,7 @@ export class ServerConsentView extends TemplateView {
])
])
]);
}
}
_onSubmit(evt) {
evt.preventDefault();
+3 -3
View File
@@ -22,13 +22,13 @@ import {getLabelForLinkKind} from "../Link.js";
import {orderedUnique} from "../utils/unique.js";
export class ServerConsentViewModel extends ViewModel {
constructor(options) {
super(options);
constructor(options) {
super(options);
this.servers = options.servers;
this.done = options.done;
this.selectedServer = this.servers[0];
this.showSelectServer = false;
}
}
setShowServers() {
this.showSelectServer = true;
+47 -47
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import {Maturity, Platform, LinkKind,
FDroidLink, AppleStoreLink, PlayStoreLink, WebsiteLink} from "../types.js";
FDroidLink, AppleStoreLink, PlayStoreLink, WebsiteLink} from "../types.js";
const trustedWebInstances = [
"app.element.io", // first one is the default one
@@ -29,69 +29,69 @@ const trustedWebInstances = [
* Information on how to deep link to a given matrix client.
*/
export class Element {
get id() { return "element.io"; }
get id() { return "element.io"; }
get platforms() {
return [
Platform.Android, Platform.iOS,
Platform.Windows, Platform.macOS, Platform.Linux,
Platform.DesktopWeb
];
}
get platforms() {
return [
Platform.Android, Platform.iOS,
Platform.Windows, Platform.macOS, Platform.Linux,
Platform.DesktopWeb
];
}
get icon() { return "images/client-icons/element.svg"; }
get appleAssociatedAppId() { return "7J4U792NQT.im.vector.app"; }
get name() {return "Element"; }
get description() { return 'Fully-featured Matrix client, used by millions.'; }
get homepage() { return "https://element.io"; }
get author() { return "Element"; }
getMaturity(platform) { return Maturity.Stable; }
get name() {return "Element"; }
get description() { return 'Fully-featured Matrix client, used by millions.'; }
get homepage() { return "https://element.io"; }
get author() { return "Element"; }
getMaturity(platform) { return Maturity.Stable; }
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;
}
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;
}
const isWebPlatform = platform === Platform.DesktopWeb || platform === Platform.MobileWeb;
if (isWebPlatform || platform === Platform.iOS) {
if (isWebPlatform || platform === Platform.iOS) {
let instanceHost = trustedWebInstances[0];
// we use app.element.io which iOS will intercept, but it likely won't intercept any other trusted instances
// so only use a preferred web instance for true web links.
if (isWebPlatform && trustedWebInstances.includes(link.webInstances[this.id])) {
instanceHost = link.webInstances[this.id];
}
return `https://${instanceHost}/#/${fragmentPath}`;
} else if (platform === Platform.Linux || platform === Platform.Windows || platform === Platform.macOS) {
return `element://vector/webapp/#/${fragmentPath}`;
} else {
return `https://${instanceHost}/#/${fragmentPath}`;
} else if (platform === Platform.Linux || platform === Platform.Windows || platform === Platform.macOS) {
return `element://vector/webapp/#/${fragmentPath}`;
} else {
return `element://${fragmentPath}`;
}
}
}
getLinkInstructions(platform, link) {}
getLinkInstructions(platform, link) {}
getCopyString(platform, link) {}
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")];
}
}
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")];
}
}
canInterceptMatrixToLinks(platform) {
return platform === Platform.Android;
}
canInterceptMatrixToLinks(platform) {
return platform === Platform.Android;
}
getPreferredWebInstance(link) {
const idx = trustedWebInstances.indexOf(link.webInstances[this.id])
+10 -10
View File
@@ -20,22 +20,22 @@ import {Maturity, Platform, LinkKind, FlathubLink} from "../types.js";
* Information on how to deep link to a given matrix client.
*/
export class Fractal {
get id() { return "fractal"; }
get name() { return "Fractal"; }
get id() { return "fractal"; }
get name() { return "Fractal"; }
get icon() { return "images/client-icons/fractal.png"; }
get author() { return "Daniel Garcia Moreno"; }
get homepage() { return "https://gitlab.gnome.org/GNOME/fractal"; }
get platforms() { return [Platform.Linux]; }
get description() { return 'Fractal is a Matrix Client written in Rust.'; }
getMaturity(platform) { return Maturity.Beta; }
getDeepLink(platform, link) {}
canInterceptMatrixToLinks(platform) { return false; }
get platforms() { return [Platform.Linux]; }
get description() { return 'Fractal is a Matrix Client written in Rust.'; }
getMaturity(platform) { return Maturity.Beta; }
getDeepLink(platform, link) {}
canInterceptMatrixToLinks(platform) { return false; }
getLinkInstructions(platform, link) {
getLinkInstructions(platform, link) {
if (link.kind === LinkKind.User || link.kind === LinkKind.Room) {
return "Click the '+' button in the top right and paste the identifier";
}
}
}
getCopyString(platform, link) {
if (link.kind === LinkKind.User || link.kind === LinkKind.Room) {
@@ -43,7 +43,7 @@ export class Fractal {
}
}
getInstallLinks(platform) {
getInstallLinks(platform) {
if (platform === Platform.Linux) {
return [new FlathubLink("org.gnome.Fractal")];
}
+39 -39
View File
@@ -20,49 +20,49 @@ import {Maturity, Platform, LinkKind, FlathubLink, style} from "../types.js";
* Information on how to deep link to a given matrix client.
*/
export class Nheko {
get id() { return "nheko"; }
get name() { return "Nheko"; }
get id() { return "nheko"; }
get name() { return "Nheko"; }
get icon() { return "images/client-icons/nheko.svg"; }
get author() { return "mujx, red_sky, deepbluev7, Konstantinos Sideris"; }
get homepage() { return "https://github.com/Nheko-Reborn/nheko"; }
get platforms() { return [Platform.Windows, Platform.macOS, Platform.Linux]; }
get description() { return 'A native desktop app for Matrix that feels more like a mainstream chat app.'; }
getMaturity(platform) { return Maturity.Beta; }
getDeepLink(platform, link) {
if (platform === Platform.Linux || platform === Platform.Windows) {
let identifier = encodeURIComponent(link.identifier.substring(1));
let isRoomid = link.identifier.substring(0, 1) === '!';
let fragmentPath;
switch (link.kind) {
case LinkKind.User:
fragmentPath = `u/${identifier}?action=chat`;
break;
case LinkKind.Room:
case LinkKind.Event:
if (isRoomid)
fragmentPath = `roomid/${identifier}`;
else
fragmentPath = `r/${identifier}`;
get platforms() { return [Platform.Windows, Platform.macOS, Platform.Linux]; }
get description() { return 'A native desktop app for Matrix that feels more like a mainstream chat app.'; }
getMaturity(platform) { return Maturity.Beta; }
getDeepLink(platform, link) {
if (platform === Platform.Linux || platform === Platform.Windows) {
let identifier = encodeURIComponent(link.identifier.substring(1));
let isRoomid = link.identifier.substring(0, 1) === '!';
let fragmentPath;
switch (link.kind) {
case LinkKind.User:
fragmentPath = `u/${identifier}?action=chat`;
break;
case LinkKind.Room:
case LinkKind.Event:
if (isRoomid)
fragmentPath = `roomid/${identifier}`;
else
fragmentPath = `r/${identifier}`;
if (link.kind === LinkKind.Event)
fragmentPath += `/e/${encodeURIComponent(link.eventId.substring(1))}`;
fragmentPath += '?action=join';
fragmentPath += link.servers.map(server => `&via=${encodeURIComponent(server)}`).join('');
break;
case LinkKind.Group:
return;
}
return `matrix:${fragmentPath}`;
}
}
canInterceptMatrixToLinks(platform) { return false; }
if (link.kind === LinkKind.Event)
fragmentPath += `/e/${encodeURIComponent(link.eventId.substring(1))}`;
fragmentPath += '?action=join';
fragmentPath += link.servers.map(server => `&via=${encodeURIComponent(server)}`).join('');
break;
case LinkKind.Group:
return;
}
return `matrix:${fragmentPath}`;
}
}
canInterceptMatrixToLinks(platform) { return false; }
getLinkInstructions(platform, link) {
switch (link.kind) {
case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)];
case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)];
}
}
getLinkInstructions(platform, link) {
switch (link.kind) {
case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)];
case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)];
}
}
getCopyString(platform, link) {
switch (link.kind) {
@@ -71,7 +71,7 @@ export class Nheko {
}
}
getInstallLinks(platform) {
getInstallLinks(platform) {
if (platform === Platform.Linux) {
return [new FlathubLink("io.github.NhekoReborn.Nheko")];
}
+14 -14
View File
@@ -20,23 +20,23 @@ import {Maturity, Platform, LinkKind, WebsiteLink, style} from "../types.js";
* Information on how to deep link to a given matrix client.
*/
export class Weechat {
get id() { return "weechat"; }
get name() { return "Weechat"; }
get id() { return "weechat"; }
get name() { return "Weechat"; }
get icon() { return "images/client-icons/weechat.svg"; }
get author() { return "Poljar"; }
get homepage() { return "https://github.com/poljar/weechat-matrix"; }
get platforms() { return [Platform.Windows, Platform.macOS, Platform.Linux]; }
get description() { return 'Command-line Matrix interface using Weechat.'; }
getMaturity(platform) { return Maturity.Beta; }
getDeepLink(platform, link) {}
canInterceptMatrixToLinks(platform) { return false; }
get platforms() { return [Platform.Windows, Platform.macOS, Platform.Linux]; }
get description() { return 'Command-line Matrix interface using Weechat.'; }
getMaturity(platform) { return Maturity.Beta; }
getDeepLink(platform, link) {}
canInterceptMatrixToLinks(platform) { return false; }
getLinkInstructions(platform, link) {
switch (link.kind) {
case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)];
case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)];
}
}
getLinkInstructions(platform, link) {
switch (link.kind) {
case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)];
case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)];
}
}
getCopyString(platform, link) {
switch (link.kind) {
@@ -45,7 +45,7 @@ export class Weechat {
}
}
getInstallLinks(platform) {}
getInstallLinks(platform) {}
getPreferredWebInstance(link) {}
}
+9 -9
View File
@@ -23,13 +23,13 @@ import {Tensor} from "./Tensor.js";
import {Fluffychat} from "./Fluffychat.js";
export function createClients() {
return [
new Element(),
new Weechat(),
new Nheko(),
new Fractal(),
new Quaternion(),
new Tensor(),
new Fluffychat(),
];
return [
new Element(),
new Weechat(),
new Nheko(),
new Fractal(),
new Quaternion(),
new Tensor(),
new Fluffychat(),
];
}
+5 -5
View File
@@ -21,8 +21,8 @@ export {Platform} from "../Platform.js";
export class AppleStoreLink {
constructor(org, appId) {
this._org = org;
this._appId = appId;
this._org = org;
this._appId = appId;
}
createInstallURL(link) {
@@ -40,7 +40,7 @@ export class AppleStoreLink {
export class PlayStoreLink {
constructor(appId) {
this._appId = appId;
this._appId = appId;
}
createInstallURL(link) {
@@ -58,7 +58,7 @@ export class PlayStoreLink {
export class FDroidLink {
constructor(appId) {
this._appId = appId;
this._appId = appId;
}
createInstallURL(link) {
@@ -94,7 +94,7 @@ export class FlathubLink {
export class WebsiteLink {
constructor(url) {
this._url = url;
this._url = url;
}
createInstallURL(link) {
+5 -5
View File
@@ -17,10 +17,10 @@ limitations under the License.
import {TemplateView} from "../utils/TemplateView.js";
export class LoadServerPolicyView extends TemplateView {
render(t, vm) {
return t.div({className: "LoadServerPolicyView card"}, [
t.div({className: {spinner: true, hidden: vm => !vm.loading}}),
render(t, vm) {
return t.div({className: "LoadServerPolicyView card"}, [
t.div({className: {spinner: true, hidden: vm => !vm.loading}}),
t.h2(vm => vm.message)
]);
}
]);
}
}
+4 -4
View File
@@ -18,12 +18,12 @@ import {ViewModel} from "../utils/ViewModel.js";
import {resolveServer} from "../preview/HomeServer.js";
export class LoadServerPolicyViewModel extends ViewModel {
constructor(options) {
super(options);
this.server = options.server;
constructor(options) {
super(options);
this.server = options.server;
this.message = `Looking up ${this.server} privacy policy…`;
this.loading = false;
}
}
async load() {
this.loading = true;
+46 -46
View File
@@ -20,11 +20,11 @@ function noTrailingSlash(url) {
export async function resolveServer(request, baseURL) {
baseURL = noTrailingSlash(baseURL);
if (!baseURL.startsWith("http://") && !baseURL.startsWith("https://")) {
baseURL = `https://${baseURL}`;
}
{
try {
if (!baseURL.startsWith("http://") && !baseURL.startsWith("https://")) {
baseURL = `https://${baseURL}`;
}
{
try {
const {status, body} = await request(`${baseURL}/.well-known/matrix/client`, {method: "GET"}).response();
if (status === 200) {
const proposedBaseURL = body?.['m.homeserver']?.base_url;
@@ -33,31 +33,31 @@ export async function resolveServer(request, baseURL) {
}
}
} catch (e) {
console.warn("Failed to fetch ${baseURL}/.well-known/matrix/client", e);
console.warn("Failed to fetch ${baseURL}/.well-known/matrix/client", e);
}
}
{
const {status} = await request(`${baseURL}/_matrix/client/versions`, {method: "GET"}).response();
if (status !== 200) {
throw new Error(`Invalid versions response from ${baseURL}`);
}
}
return new HomeServer(request, baseURL);
}
{
const {status} = await request(`${baseURL}/_matrix/client/versions`, {method: "GET"}).response();
if (status !== 200) {
throw new Error(`Invalid versions response from ${baseURL}`);
}
}
return new HomeServer(request, baseURL);
}
export class HomeServer {
constructor(request, baseURL) {
this._request = request;
this.baseURL = baseURL;
}
constructor(request, baseURL) {
this._request = request;
this.baseURL = baseURL;
}
async getUserProfile(userId) {
const {body} = await this._request(`${this.baseURL}/_matrix/client/r0/profile/${encodeURIComponent(userId)}`).response();
return body;
}
async getUserProfile(userId) {
const {body} = await this._request(`${this.baseURL}/_matrix/client/r0/profile/${encodeURIComponent(userId)}`).response();
return body;
}
// MSC3266 implementation
async getRoomSummary(roomIdOrAlias, viaServers) {
async getRoomSummary(roomIdOrAlias, viaServers) {
let query;
if (viaServers.length > 0) {
query = "?" + viaServers.map(server => `via=${encodeURIComponent(server)}`).join('&');
@@ -67,29 +67,29 @@ export class HomeServer {
return body;
}
async findPublicRoomById(roomId) {
const {body, status} = await this._request(`${this.baseURL}/_matrix/client/r0/directory/list/room/${encodeURIComponent(roomId)}`).response();
if (status !== 200 || body.visibility !== "public") {
return;
}
let nextBatch;
do {
const queryParams = encodeQueryParams({limit: 10000, since: nextBatch});
const {body, status} = await this._request(`${this.baseURL}/_matrix/client/r0/publicRooms?${queryParams}`).response();
nextBatch = body.next_batch;
const publicRoom = body.chunk.find(c => c.room_id === roomId);
if (publicRoom) {
return publicRoom;
}
} while (nextBatch);
}
async findPublicRoomById(roomId) {
const {body, status} = await this._request(`${this.baseURL}/_matrix/client/r0/directory/list/room/${encodeURIComponent(roomId)}`).response();
if (status !== 200 || body.visibility !== "public") {
return;
}
let nextBatch;
do {
const queryParams = encodeQueryParams({limit: 10000, since: nextBatch});
const {body, status} = await this._request(`${this.baseURL}/_matrix/client/r0/publicRooms?${queryParams}`).response();
nextBatch = body.next_batch;
const publicRoom = body.chunk.find(c => c.room_id === roomId);
if (publicRoom) {
return publicRoom;
}
} while (nextBatch);
}
async getRoomIdFromAlias(alias) {
const {status, body} = await this._request(`${this.baseURL}/_matrix/client/r0/directory/room/${encodeURIComponent(alias)}`).response();
if (status === 200) {
return body.room_id;
}
}
async getRoomIdFromAlias(alias) {
const {status, body} = await this._request(`${this.baseURL}/_matrix/client/r0/directory/room/${encodeURIComponent(alias)}`).response();
if (status === 200) {
return body.room_id;
}
}
async getPrivacyPolicyUrl(lang = "en") {
const headers = new Map();
@@ -109,7 +109,7 @@ export class HomeServer {
}
}
mxcUrlThumbnail(url, width, height, method) {
mxcUrlThumbnail(url, width, height, method) {
const parts = parseMxcUrl(url);
if (parts) {
const [serverName, mediaId] = parts;
+9 -9
View File
@@ -43,7 +43,7 @@ class LoadingPreviewView extends TemplateView {
}
class LoadedPreviewView extends TemplateView {
render(t, vm) {
render(t, vm) {
const avatar = t.map(vm => vm.avatarUrl, (avatarUrl, t) => {
if (avatarUrl) {
return t.img({className: "avatar", src: avatarUrl});
@@ -51,12 +51,12 @@ class LoadedPreviewView extends TemplateView {
return t.div({className: "defaultAvatar"});
}
});
return t.div({className: vm.isSpaceRoom ? "mxSpace" : undefined}, [
t.div({className: "avatarContainer"}, avatar),
t.h1(vm => vm.name),
t.p({className: {identifier: true, hidden: vm => !vm.identifier}}, vm => vm.identifier),
t.div({className: {memberCount: true, hidden: vm => !vm.memberCount}}, t.p([vm => vm.memberCount, " members"])),
t.p({className: {topic: true, hidden: vm => !vm.topic}}, [vm => vm.topic]),
]);
}
return t.div({className: vm.isSpaceRoom ? "mxSpace" : undefined}, [
t.div({className: "avatarContainer"}, avatar),
t.h1(vm => vm.name),
t.p({className: {identifier: true, hidden: vm => !vm.identifier}}, vm => vm.identifier),
t.div({className: {memberCount: true, hidden: vm => !vm.memberCount}}, t.p([vm => vm.memberCount, " members"])),
t.p({className: {topic: true, hidden: vm => !vm.topic}}, [vm => vm.topic]),
]);
}
}
+57 -57
View File
@@ -21,79 +21,79 @@ import {ClientListViewModel} from "../open/ClientListViewModel.js";
import {ClientViewModel} from "../open/ClientViewModel.js";
export class PreviewViewModel extends ViewModel {
constructor(options) {
super(options);
const { link, consentedServers } = options;
this._link = link;
this._consentedServers = consentedServers;
this.loading = false;
this.name = this._link.identifier;
this.avatarUrl = null;
this.identifier = null;
this.memberCount = null;
this.topic = null;
this.domain = null;
constructor(options) {
super(options);
const { link, consentedServers } = options;
this._link = link;
this._consentedServers = consentedServers;
this.loading = false;
this.name = this._link.identifier;
this.avatarUrl = null;
this.identifier = null;
this.memberCount = null;
this.topic = null;
this.domain = null;
this.failed = false;
this.isSpaceRoom = false;
}
}
async load() {
async load() {
const {kind} = this._link;
const supportsPreview = kind === LinkKind.User || kind === LinkKind.Room || kind === LinkKind.Event;
if (supportsPreview) {
this.loading = true;
this.emitChange();
for (const server of this._consentedServers) {
try {
const homeserver = await resolveServer(this.request, server);
switch (this._link.kind) {
case LinkKind.User:
await this._loadUserPreview(homeserver, this._link.identifier);
break;
case LinkKind.Room:
this.loading = true;
this.emitChange();
for (const server of this._consentedServers) {
try {
const homeserver = await resolveServer(this.request, server);
switch (this._link.kind) {
case LinkKind.User:
await this._loadUserPreview(homeserver, this._link.identifier);
break;
case LinkKind.Room:
case LinkKind.Event:
await this._loadRoomPreview(homeserver, this._link);
break;
}
// assume we're done if nothing threw
this.domain = server;
await this._loadRoomPreview(homeserver, this._link);
break;
}
// assume we're done if nothing threw
this.domain = server;
this.loading = false;
this.emitChange();
this.emitChange();
return;
} catch (err) {
continue;
}
}
} catch (err) {
continue;
}
}
}
this.loading = false;
this._setNoPreview(this._link);
this._setNoPreview(this._link);
if (this._consentedServers.length && supportsPreview) {
this.domain = this._consentedServers[this._consentedServers.length - 1];
this.failed = true;
}
this.emitChange();
}
}
get hasTopic() { return this._link.kind === LinkKind.Room; }
get hasMemberCount() { return this.hasTopic; }
async _loadUserPreview(homeserver, userId) {
const profile = await homeserver.getUserProfile(userId);
this.name = profile.displayname || userId;
this.avatarUrl = profile.avatar_url ?
homeserver.mxcUrlThumbnail(profile.avatar_url, 64, 64, "crop") :
null;
this.identifier = userId;
}
async _loadUserPreview(homeserver, userId) {
const profile = await homeserver.getUserProfile(userId);
this.name = profile.displayname || userId;
this.avatarUrl = profile.avatar_url ?
homeserver.mxcUrlThumbnail(profile.avatar_url, 64, 64, "crop") :
null;
this.identifier = userId;
}
async _loadRoomPreview(homeserver, link) {
let publicRoom;
if (link.identifierKind === IdentifierKind.RoomId || link.identifierKind === IdentifierKind.RoomAlias) {
async _loadRoomPreview(homeserver, link) {
let publicRoom;
if (link.identifierKind === IdentifierKind.RoomId || link.identifierKind === IdentifierKind.RoomAlias) {
publicRoom = await homeserver.getRoomSummary(link.identifier, link.servers);
}
if (!publicRoom) {
if (!publicRoom) {
if (link.identifierKind === IdentifierKind.RoomId) {
publicRoom = await homeserver.findPublicRoomById(link.identifier);
} else if (link.identifierKind === IdentifierKind.RoomAlias) {
@@ -104,18 +104,18 @@ export class PreviewViewModel extends ViewModel {
}
}
this.name = publicRoom?.name || publicRoom?.canonical_alias || link.identifier;
this.avatarUrl = publicRoom?.avatar_url ?
homeserver.mxcUrlThumbnail(publicRoom.avatar_url, 64, 64, "crop") :
null;
this.memberCount = publicRoom?.num_joined_members;
this.topic = publicRoom?.topic;
this.identifier = publicRoom?.canonical_alias || link.identifier;
this.isSpaceRoom = publicRoom?.room_type === "m.space";
this.name = publicRoom?.name || publicRoom?.canonical_alias || link.identifier;
this.avatarUrl = publicRoom?.avatar_url ?
homeserver.mxcUrlThumbnail(publicRoom.avatar_url, 64, 64, "crop") :
null;
this.memberCount = publicRoom?.num_joined_members;
this.topic = publicRoom?.topic;
this.identifier = publicRoom?.canonical_alias || link.identifier;
this.isSpaceRoom = publicRoom?.room_type === "m.space";
if (this.identifier === this.name) {
this.identifier = null;
}
}
}
_setNoPreview(link) {
this.name = link.identifier;