Merge pull request #219 from matrix-org/t3chguy/msc3266

This commit is contained in:
Michael Telatynski 2021-08-13 17:00:19 +01:00 committed by GitHub
commit c25a9dae4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 828 additions and 798 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules
build
*.tar.gz
/.idea

View file

@ -21,31 +21,31 @@ limitations under the License.
@import url('open.css');
:root {
--app-background: #f4f4f4;
--background: #ffffff;
--foreground: #000000;
--font: #333333;
--grey: #666666;
--accent: #0098d4;
--error: #d6001c;
--link: #0098d4;
--borders: #f4f4f4;
--app-background: #f4f4f4;
--background: #ffffff;
--foreground: #000000;
--font: #333333;
--grey: #666666;
--accent: #0098d4;
--error: #d6001c;
--link: #0098d4;
--borders: #f4f4f4;
--lightgrey: #E6E6E6;
--spinner-stroke-size: 2px;
}
html {
margin: 0;
padding: 0;
margin: 0;
padding: 0;
}
body {
background-color: var(--app-background);
background-image: url('../images/background.svg');
background-color: var(--app-background);
background-image: url('../images/background.svg');
background-attachment: fixed;
background-repeat: no-repeat;
background-size: auto;
background-position: center -50px;
background-repeat: no-repeat;
background-size: auto;
background-position: center -50px;
height: 100%;
width: 100%;
font-size: 14px;
@ -89,12 +89,12 @@ input[type="checkbox"], input[type="radio"] {
.RootView {
margin: 0 auto;
max-width: 480px;
width: 100%;
max-width: 480px;
width: 100%;
}
.card {
background-color: var(--background);
background-color: var(--background);
border-radius: 16px;
box-shadow: 0px 18px 24px rgba(0, 0, 0, 0.06);
}
@ -104,20 +104,20 @@ input[type="checkbox"], input[type="radio"] {
}
.hidden {
display: none !important;
display: none !important;
}
@media screen and (max-width: 480px) {
body {
background-image: none;
background-color: var(--background);
padding: 0;
background-image: none;
background-color: var(--background);
padding: 0;
}
.card {
border-radius: unset;
box-shadow: unset;
border-radius: unset;
box-shadow: unset;
}
}
@ -141,7 +141,7 @@ input[type="checkbox"], input[type="radio"] {
}
a, button.text {
color: var(--link);
color: var(--link);
}
button.text {

View file

@ -22,6 +22,10 @@
height: 64px;
}
.PreviewView .mxSpace .avatar {
border-radius: 12px;
}
.PreviewView .defaultAvatar {
width: 64px;
height: 64px;

View file

@ -1,43 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 181.4 181.9" style="enable-background:new 0 0 181.4 181.9;" xml:space="preserve">
viewBox="0 0 181.4 181.9" style="enable-background:new 0 0 181.4 181.9;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#F094BE;}
.st2{fill:#4D3F92;}
.st3{fill:#FFFFFF;}
.st0{fill:url(#SVGID_1_);}
.st1{fill:#F094BE;}
.st2{fill:#4D3F92;}
.st3{fill:#FFFFFF;}
</style>
<g id="Capa_1">
<rect x="0" y="0" style="color:#FFFFFF" width="181.4" height="181.9" class="st3"/>
<rect x="0" y="0" style="color:#FFFFFF" width="181.4" height="181.9" class="st3"/>
</g>
<g id="Capa_2">
<g>
<path class="st2" d="M151.6,95.1c1.5-0.3,2.8-1,3.8-2c4-5.3,0.8-11.8-4.5-12.6c-0.8,0-1.5-0.8-1.5-1.5c0-0.3,0-0.5,0-0.5
c0.8-0.8,1.5-1.8,2.5-3.3c8.1-10.8,11.8-50.6,3.8-53.7c-9.8-3.3-29.7,6.3-38.3,17.4c-0.5-0.3-1-1-1-1.8c0.3-3-1.3-5.5-3.5-6.8
c-4.5-2.3-8.8,0-10.6,3.3c-0.5,0.8-1.3,1.3-2,1c-0.8,0-1.5-0.8-1.5-1.5c-0.5-2.5-2-4.5-4.3-5.5c-4.8-2-9.8,0.8-10.6,5.3
c-0.3,0.8-0.8,1.5-1.5,1.5c-0.8,0.3-1.5-0.3-2-1c-1.5-2.3-4-3.8-6.5-3.8c-4,0-7.6,3.3-7.8,7.3v0.3v0.3c0,0.8-0.5,1.5-1,1.8h-0.3
c-8.3-10.8-28.5-20.7-38.5-17.4c-8.1,2.8-4.3,42.6,4,53.4c1.5,2,2.8,3.5,3.8,4.5c-0.3,0.8-1,1.5-1.8,1.5c-1.3,0-2.5,0.5-3.5,1.3
c-5.3,5-2.3,12.1,3,13.4c0.8,0.3,1.5,1,1.5,1.8c0,0.8-0.5,1.8-1.3,2c-1,0.5-2,1-2.8,2c-4,5.8,0,12.3,5.5,12.3
c0.8,0,1.5,0.5,1.8,1.3c0.3,0.8,0.3,1.5-0.5,2c-1.5,1.5-2.3,3.5-2,5.5c0.3,2.8,2,5.3,4.8,6.5c1.5,0.8,3,0.8,4.5,0.5
c0.8-0.3,1.5,0,2,0.8c0.5,0.5,0.5,1.5,0.3,2c-0.8,1.5-1,3.3-0.5,5c0.8,2.8,2.8,4.8,5.5,5.5c2.5,0.5,4.3-0.3,5.5-0.8
c0.5-0.3-3.3,9.1-6,15.4c-0.8,2,1.3,4.3,3.5,3.3c8.3-3.8,22.2-10.3,22.2-9.8c0.5,5.3,6.5,9.1,12.3,5.3c1.3-0.8,2-2.3,2.3-3.5
c0.3-0.8,1-1.5,2-1.5c1,0,1.8,0.5,2,1.5c0.3,1.3,0.8,2.3,1.8,3c5.8,4.5,12.3,0.8,12.8-4.8c0-0.8,0.5-1.5,1.3-1.8
c0.8-0.3,1.5,0,2,0.5c1.5,1.5,3.3,2.5,5.3,2.5l0,0c2.5,0,5-1.3,6.5-3.8c1-1.5,1.3-3,1-5c0-0.8,0.3-1.5,0.8-2c0.5-0.5,1.5-0.5,2,0
c1.5,0.8,3.3,1.3,5,0.8c2.8-0.5,5-2.8,5.8-5.3c0.5-1.8,0.3-3.5-0.5-5.3c-0.3-0.8-0.3-1.5,0.3-2s1.3-0.8,2-0.8
c1.8,0.3,3.3,0.3,4.8-0.5c2.3-1,3.8-3,4.3-5.5c0.5-2.5-0.3-4.8-2-6.5c-0.5-0.5-0.8-1.3-0.5-2s1-1.3,1.8-1.3c1.8,0,3.8-0.5,5-2
c4.3-4.5,2.3-10.6-2.5-12.6c-0.8-0.3-1.3-1-1.3-2C150.1,95.8,150.8,95.1,151.6,95.1z"/>
<path class="st3" d="M131.4,42.2c0.5,1.5,0.5,3,0,4.5c-0.3,0.8,0,1.5,0.5,2s1.3,0.8,2,0.5c1-0.5,2-0.5,3-0.5c2.3,0,4.3,1,5.8,3
c1,1.3,1.8,3,1.5,4.8c0,1.5-0.5,2.8-1.3,4c-0.5,0.5-0.5,1.5,0,2c0.3,0.3,0.5,0.8,1,0.8c1-0.3,2-1,2.8-2c4.5-6.3,5.3-26.2,0.8-27.7
c-4.5-1.5-12.3,1.5-17.9,6C130.7,40.1,131.2,40.9,131.4,42.2z"/>
<path class="st3" d="M39,63.6c0.3-0.3,0.5-0.5,0.8-0.8c0.5-0.8,0.3-1.5,0-2C38.5,59,38.2,57,38.5,55c0.5-2.8,2.8-5,5.5-5.8
c1.5-0.5,3-0.3,4.5,0.3c0.8,0.3,1.5,0,2-0.5c0.5-0.5,0.8-1.3,0.5-2c-0.5-1.5-0.5-3,0-4.5c0.3-1,0.8-2,1.5-2.8
c-5.5-4.5-13.9-7.8-18.4-6.3S30.4,54.8,35,61.1C36,62.6,37.2,63.3,39,63.6z"/>
<g>
<circle class="st3" cx="60.9" cy="94.6" r="9.3"/>
<path class="st3" d="M100.7,94.6c0,5.3-4.3,9.3-9.3,9.3c-5.3,0-9.3-4.3-9.3-9.3S100.7,89.3,100.7,94.6z"/>
<circle class="st3" cx="121.6" cy="94.6" r="9.3"/>
</g>
</g>
<g>
<path class="st2" d="M151.6,95.1c1.5-0.3,2.8-1,3.8-2c4-5.3,0.8-11.8-4.5-12.6c-0.8,0-1.5-0.8-1.5-1.5c0-0.3,0-0.5,0-0.5
c0.8-0.8,1.5-1.8,2.5-3.3c8.1-10.8,11.8-50.6,3.8-53.7c-9.8-3.3-29.7,6.3-38.3,17.4c-0.5-0.3-1-1-1-1.8c0.3-3-1.3-5.5-3.5-6.8
c-4.5-2.3-8.8,0-10.6,3.3c-0.5,0.8-1.3,1.3-2,1c-0.8,0-1.5-0.8-1.5-1.5c-0.5-2.5-2-4.5-4.3-5.5c-4.8-2-9.8,0.8-10.6,5.3
c-0.3,0.8-0.8,1.5-1.5,1.5c-0.8,0.3-1.5-0.3-2-1c-1.5-2.3-4-3.8-6.5-3.8c-4,0-7.6,3.3-7.8,7.3v0.3v0.3c0,0.8-0.5,1.5-1,1.8h-0.3
c-8.3-10.8-28.5-20.7-38.5-17.4c-8.1,2.8-4.3,42.6,4,53.4c1.5,2,2.8,3.5,3.8,4.5c-0.3,0.8-1,1.5-1.8,1.5c-1.3,0-2.5,0.5-3.5,1.3
c-5.3,5-2.3,12.1,3,13.4c0.8,0.3,1.5,1,1.5,1.8c0,0.8-0.5,1.8-1.3,2c-1,0.5-2,1-2.8,2c-4,5.8,0,12.3,5.5,12.3
c0.8,0,1.5,0.5,1.8,1.3c0.3,0.8,0.3,1.5-0.5,2c-1.5,1.5-2.3,3.5-2,5.5c0.3,2.8,2,5.3,4.8,6.5c1.5,0.8,3,0.8,4.5,0.5
c0.8-0.3,1.5,0,2,0.8c0.5,0.5,0.5,1.5,0.3,2c-0.8,1.5-1,3.3-0.5,5c0.8,2.8,2.8,4.8,5.5,5.5c2.5,0.5,4.3-0.3,5.5-0.8
c0.5-0.3-3.3,9.1-6,15.4c-0.8,2,1.3,4.3,3.5,3.3c8.3-3.8,22.2-10.3,22.2-9.8c0.5,5.3,6.5,9.1,12.3,5.3c1.3-0.8,2-2.3,2.3-3.5
c0.3-0.8,1-1.5,2-1.5c1,0,1.8,0.5,2,1.5c0.3,1.3,0.8,2.3,1.8,3c5.8,4.5,12.3,0.8,12.8-4.8c0-0.8,0.5-1.5,1.3-1.8
c0.8-0.3,1.5,0,2,0.5c1.5,1.5,3.3,2.5,5.3,2.5l0,0c2.5,0,5-1.3,6.5-3.8c1-1.5,1.3-3,1-5c0-0.8,0.3-1.5,0.8-2c0.5-0.5,1.5-0.5,2,0
c1.5,0.8,3.3,1.3,5,0.8c2.8-0.5,5-2.8,5.8-5.3c0.5-1.8,0.3-3.5-0.5-5.3c-0.3-0.8-0.3-1.5,0.3-2s1.3-0.8,2-0.8
c1.8,0.3,3.3,0.3,4.8-0.5c2.3-1,3.8-3,4.3-5.5c0.5-2.5-0.3-4.8-2-6.5c-0.5-0.5-0.8-1.3-0.5-2s1-1.3,1.8-1.3c1.8,0,3.8-0.5,5-2
c4.3-4.5,2.3-10.6-2.5-12.6c-0.8-0.3-1.3-1-1.3-2C150.1,95.8,150.8,95.1,151.6,95.1z"/>
<path class="st3" d="M131.4,42.2c0.5,1.5,0.5,3,0,4.5c-0.3,0.8,0,1.5,0.5,2s1.3,0.8,2,0.5c1-0.5,2-0.5,3-0.5c2.3,0,4.3,1,5.8,3
c1,1.3,1.8,3,1.5,4.8c0,1.5-0.5,2.8-1.3,4c-0.5,0.5-0.5,1.5,0,2c0.3,0.3,0.5,0.8,1,0.8c1-0.3,2-1,2.8-2c4.5-6.3,5.3-26.2,0.8-27.7
c-4.5-1.5-12.3,1.5-17.9,6C130.7,40.1,131.2,40.9,131.4,42.2z"/>
<path class="st3" d="M39,63.6c0.3-0.3,0.5-0.5,0.8-0.8c0.5-0.8,0.3-1.5,0-2C38.5,59,38.2,57,38.5,55c0.5-2.8,2.8-5,5.5-5.8
c1.5-0.5,3-0.3,4.5,0.3c0.8,0.3,1.5,0,2-0.5c0.5-0.5,0.8-1.3,0.5-2c-0.5-1.5-0.5-3,0-4.5c0.3-1,0.8-2,1.5-2.8
c-5.5-4.5-13.9-7.8-18.4-6.3S30.4,54.8,35,61.1C36,62.6,37.2,63.3,39,63.6z"/>
<g>
<circle class="st3" cx="60.9" cy="94.6" r="9.3"/>
<path class="st3" d="M100.7,94.6c0,5.3-4.3,9.3-9.3,9.3c-5.3,0-9.3-4.3-9.3-9.3S100.7,89.3,100.7,94.6z"/>
<circle class="st3" cx="121.6" cy="94.6" r="9.3"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -1,17 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>You're invited to talk on Matrix</title>
<meta name="description" content="You're invited to talk on Matrix">
<meta name="viewport" content="width=device-width, user-scalable=no">
<link rel="stylesheet" type="text/css" href="css/main.css">
<meta charset="utf-8">
<title>You're invited to talk on Matrix</title>
<meta name="description" content="You're invited to talk on Matrix">
<meta name="viewport" content="width=device-width, user-scalable=no">
<link rel="stylesheet" type="text/css" href="css/main.css">
</head>
<body>
<script id="main" type="module">
import {main} from "./src/main.js";
main(document.body);
</script>
<script id="main" type="module">
import {main} from "./src/main.js";
main(document.body);
</script>
<noscript>
<h1>Please enable javascript</h1>
<p>Matrix.to is a preview service from chat rooms, people and communities on <a href="https://matrix.org">Matrix</a>.</p>

View file

@ -23,25 +23,26 @@ const projectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".
// Serve up parent directory with cache disabled
const serve = serveStatic(
projectDir,
{
etag: false,
setHeaders: res => {
res.setHeader("Pragma", "no-cache");
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Expires", "Wed, 21 Oct 2015 07:28:00 GMT");
projectDir,
{
etag: false,
setHeaders: res => {
res.setHeader("Pragma", "no-cache");
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Expires", "Wed, 21 Oct 2015 07:28:00 GMT");
// same CSP as matrix.to server is using, so local testing happens under similar environment
res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src * data:; connect-src *; font-src 'self'; manifest-src 'self'; form-action 'self'; navigate-to *;");
},
index: ['index.html', 'index.htm']
}
},
index: ['index.html', 'index.htm']
}
);
// Create server
const server = http.createServer(function onRequest (req, res) {
console.log(req.method, req.url);
serve(req, res, finalhandler(req, res))
serve(req, res, finalhandler(req, res))
});
// Listen
server.listen(5000);
console.log("Listening on port 5000");

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}`;
}
}
}

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;
}

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;
}
}

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);
}

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;
}
}

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;

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);

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));
});
}

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)))
]);
}
}

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();
}
}

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) {

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;

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 {

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;

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();

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;

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])

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")];
}

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")];
}

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) {}
}

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(),
];
}

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) {

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)
]);
}
]);
}
}

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;

View file

@ -20,61 +20,76 @@ function noTrailingSlash(url) {
export async function resolveServer(request, baseURL) {
baseURL = noTrailingSlash(baseURL);
if (!baseURL.startsWith("http://") && !baseURL.startsWith("https://")) {
baseURL = `https://${baseURL}`;
}
{
const {status, body} = await request(`${baseURL}/.well-known/matrix/client`, {method: "GET"}).response();
if (status === 200) {
const proposedBaseURL = body?.['m.homeserver']?.base_url;
if (typeof proposedBaseURL === "string") {
baseURL = noTrailingSlash(proposedBaseURL);
}
}
}
{
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);
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;
if (typeof proposedBaseURL === "string") {
baseURL = noTrailingSlash(proposedBaseURL);
}
}
} catch (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);
}
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;
}
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);
}
// MSC3266 implementation
async getRoomSummary(roomIdOrAlias, viaServers) {
let query;
if (viaServers.length > 0) {
query = "?" + viaServers.map(server => `via=${encodeURIComponent(server)}`).join('&');
}
const {body, status} = await this._request(`${this.baseURL}/_matrix/client/unstable/im.nheko.summary/rooms/${encodeURIComponent(roomIdOrAlias)}/summary${query}`).response();
if (status !== 200) return;
return body;
}
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 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 getPrivacyPolicyUrl(lang = "en") {
const headers = new Map();
@ -94,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;

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([
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]),
]);
}
}

View file

@ -21,92 +21,101 @@ 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) {
publicRoom = await homeserver.findPublicRoomById(link.identifier);
} else if (link.identifierKind === IdentifierKind.RoomAlias) {
const roomId = await homeserver.getRoomIdFromAlias(link.identifier);
if (roomId) {
publicRoom = await homeserver.findPublicRoomById(roomId);
}
}
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;
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 (link.identifierKind === IdentifierKind.RoomId) {
publicRoom = await homeserver.findPublicRoomById(link.identifier);
} else if (link.identifierKind === IdentifierKind.RoomAlias) {
const roomId = await homeserver.getRoomIdFromAlias(link.identifier);
if (roomId) {
publicRoom = await homeserver.findPublicRoomById(roomId);
}
}
}
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;