Merge pull request #219 from matrix-org/t3chguy/msc3266
This commit is contained in:
commit
c25a9dae4d
33 changed files with 828 additions and 798 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
/.idea
|
||||||
|
|
52
css/main.css
52
css/main.css
|
@ -21,31 +21,31 @@ limitations under the License.
|
||||||
@import url('open.css');
|
@import url('open.css');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--app-background: #f4f4f4;
|
--app-background: #f4f4f4;
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #000000;
|
--foreground: #000000;
|
||||||
--font: #333333;
|
--font: #333333;
|
||||||
--grey: #666666;
|
--grey: #666666;
|
||||||
--accent: #0098d4;
|
--accent: #0098d4;
|
||||||
--error: #d6001c;
|
--error: #d6001c;
|
||||||
--link: #0098d4;
|
--link: #0098d4;
|
||||||
--borders: #f4f4f4;
|
--borders: #f4f4f4;
|
||||||
--lightgrey: #E6E6E6;
|
--lightgrey: #E6E6E6;
|
||||||
--spinner-stroke-size: 2px;
|
--spinner-stroke-size: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--app-background);
|
background-color: var(--app-background);
|
||||||
background-image: url('../images/background.svg');
|
background-image: url('../images/background.svg');
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: auto;
|
background-size: auto;
|
||||||
background-position: center -50px;
|
background-position: center -50px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -89,12 +89,12 @@ input[type="checkbox"], input[type="radio"] {
|
||||||
|
|
||||||
.RootView {
|
.RootView {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0px 18px 24px rgba(0, 0, 0, 0.06);
|
box-shadow: 0px 18px 24px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
@ -104,20 +104,20 @@ input[type="checkbox"], input[type="radio"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
@media screen and (max-width: 480px) {
|
||||||
body {
|
body {
|
||||||
background-image: none;
|
background-image: none;
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border-radius: unset;
|
border-radius: unset;
|
||||||
box-shadow: unset;
|
box-shadow: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ input[type="checkbox"], input[type="radio"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
a, button.text {
|
a, button.text {
|
||||||
color: var(--link);
|
color: var(--link);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.text {
|
button.text {
|
||||||
|
|
|
@ -22,6 +22,10 @@
|
||||||
height: 64px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.PreviewView .mxSpace .avatar {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.PreviewView .defaultAvatar {
|
.PreviewView .defaultAvatar {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
|
|
|
@ -1,43 +1,43 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<!-- 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"
|
<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">
|
<style type="text/css">
|
||||||
.st0{fill:url(#SVGID_1_);}
|
.st0{fill:url(#SVGID_1_);}
|
||||||
.st1{fill:#F094BE;}
|
.st1{fill:#F094BE;}
|
||||||
.st2{fill:#4D3F92;}
|
.st2{fill:#4D3F92;}
|
||||||
.st3{fill:#FFFFFF;}
|
.st3{fill:#FFFFFF;}
|
||||||
</style>
|
</style>
|
||||||
<g id="Capa_1">
|
<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>
|
||||||
<g id="Capa_2">
|
<g id="Capa_2">
|
||||||
<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
|
<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
|
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-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-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-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
|
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,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.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.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.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
|
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.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
|
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"/>
|
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
|
<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
|
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"/>
|
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
|
<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
|
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"/>
|
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>
|
<g>
|
||||||
<circle class="st3" cx="60.9" cy="94.6" r="9.3"/>
|
<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"/>
|
<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"/>
|
<circle class="st3" cx="121.6" cy="94.6" r="9.3"/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3.3 KiB |
18
index.html
18
index.html
|
@ -1,17 +1,17 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>You're invited to talk on Matrix</title>
|
<title>You're invited to talk on Matrix</title>
|
||||||
<meta name="description" content="You're invited to talk on Matrix">
|
<meta name="description" content="You're invited to talk on Matrix">
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||||
<link rel="stylesheet" type="text/css" href="css/main.css">
|
<link rel="stylesheet" type="text/css" href="css/main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script id="main" type="module">
|
<script id="main" type="module">
|
||||||
import {main} from "./src/main.js";
|
import {main} from "./src/main.js";
|
||||||
main(document.body);
|
main(document.body);
|
||||||
</script>
|
</script>
|
||||||
<noscript>
|
<noscript>
|
||||||
<h1>Please enable javascript</h1>
|
<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>
|
<p>Matrix.to is a preview service from chat rooms, people and communities on <a href="https://matrix.org">Matrix</a>.</p>
|
||||||
|
|
|
@ -23,25 +23,26 @@ const projectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".
|
||||||
|
|
||||||
// Serve up parent directory with cache disabled
|
// Serve up parent directory with cache disabled
|
||||||
const serve = serveStatic(
|
const serve = serveStatic(
|
||||||
projectDir,
|
projectDir,
|
||||||
{
|
{
|
||||||
etag: false,
|
etag: false,
|
||||||
setHeaders: res => {
|
setHeaders: res => {
|
||||||
res.setHeader("Pragma", "no-cache");
|
res.setHeader("Pragma", "no-cache");
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
res.setHeader("Expires", "Wed, 21 Oct 2015 07:28:00 GMT");
|
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
|
// 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 *;");
|
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
|
// Create server
|
||||||
const server = http.createServer(function onRequest (req, res) {
|
const server = http.createServer(function onRequest (req, res) {
|
||||||
console.log(req.method, req.url);
|
console.log(req.method, req.url);
|
||||||
serve(req, res, finalhandler(req, res))
|
serve(req, res, finalhandler(req, res))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen
|
// Listen
|
||||||
server.listen(5000);
|
server.listen(5000);
|
||||||
|
console.log("Listening on port 5000");
|
||||||
|
|
198
src/Link.js
198
src/Link.js
|
@ -24,20 +24,20 @@ const EVENTID_PATTERN = /^$([^:]+):(.+)$/;
|
||||||
const GROUPID_PATTERN = /^\+([^:]+):(.+)$/;
|
const GROUPID_PATTERN = /^\+([^:]+):(.+)$/;
|
||||||
|
|
||||||
export const IdentifierKind = createEnum(
|
export const IdentifierKind = createEnum(
|
||||||
"RoomId",
|
"RoomId",
|
||||||
"RoomAlias",
|
"RoomAlias",
|
||||||
"UserId",
|
"UserId",
|
||||||
"GroupId",
|
"GroupId",
|
||||||
);
|
);
|
||||||
|
|
||||||
function asPrefix(identifierKind) {
|
function asPrefix(identifierKind) {
|
||||||
switch (identifierKind) {
|
switch (identifierKind) {
|
||||||
case IdentifierKind.RoomId: return "!";
|
case IdentifierKind.RoomId: return "!";
|
||||||
case IdentifierKind.RoomAlias: return "#";
|
case IdentifierKind.RoomAlias: return "#";
|
||||||
case IdentifierKind.GroupId: return "+";
|
case IdentifierKind.GroupId: return "+";
|
||||||
case IdentifierKind.UserId: return "@";
|
case IdentifierKind.UserId: return "@";
|
||||||
default: throw new Error("invalid id kind " + identifierKind);
|
default: throw new Error("invalid id kind " + identifierKind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWebInstanceMap(queryParams) {
|
function getWebInstanceMap(queryParams) {
|
||||||
|
@ -56,19 +56,19 @@ function getWebInstanceMap(queryParams) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLabelForLinkKind(kind) {
|
export function getLabelForLinkKind(kind) {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case LinkKind.User: return "Start chat";
|
case LinkKind.User: return "Start chat";
|
||||||
case LinkKind.Room: return "View room";
|
case LinkKind.Room: return "View room";
|
||||||
case LinkKind.Group: return "View community";
|
case LinkKind.Group: return "View community";
|
||||||
case LinkKind.Event: return "View message";
|
case LinkKind.Event: return "View message";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkKind = createEnum(
|
export const LinkKind = createEnum(
|
||||||
"Room",
|
"Room",
|
||||||
"User",
|
"User",
|
||||||
"Group",
|
"Group",
|
||||||
"Event"
|
"Event"
|
||||||
)
|
)
|
||||||
|
|
||||||
export class Link {
|
export class Link {
|
||||||
|
@ -81,106 +81,106 @@ export class Link {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static parse(fragment) {
|
static parse(fragment) {
|
||||||
if (!fragment) {
|
if (!fragment) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let [linkStr, queryParamsStr] = fragment.split("?");
|
let [linkStr, queryParamsStr] = fragment.split("?");
|
||||||
|
|
||||||
let viaServers = [];
|
let viaServers = [];
|
||||||
let clientId = null;
|
let clientId = null;
|
||||||
let webInstances = {};
|
let webInstances = {};
|
||||||
if (queryParamsStr) {
|
if (queryParamsStr) {
|
||||||
const queryParams = queryParamsStr.split("&").map(pair => {
|
const queryParams = queryParamsStr.split("&").map(pair => {
|
||||||
const [key, value] = pair.split("=");
|
const [key, value] = pair.split("=");
|
||||||
return [decodeURIComponent(key), decodeURIComponent(value)];
|
return [decodeURIComponent(key), decodeURIComponent(value)];
|
||||||
});
|
});
|
||||||
viaServers = queryParams
|
viaServers = queryParams
|
||||||
.filter(([key, value]) => key === "via")
|
.filter(([key, value]) => key === "via")
|
||||||
.map(([,value]) => value);
|
.map(([,value]) => value);
|
||||||
const clientParam = queryParams.find(([key]) => key === "client");
|
const clientParam = queryParams.find(([key]) => key === "client");
|
||||||
if (clientParam) {
|
if (clientParam) {
|
||||||
clientId = clientParam[1];
|
clientId = clientParam[1];
|
||||||
}
|
}
|
||||||
webInstances = getWebInstanceMap(queryParams);
|
webInstances = getWebInstanceMap(queryParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (linkStr.startsWith("#/")) {
|
if (linkStr.startsWith("#/")) {
|
||||||
linkStr = linkStr.substr(2);
|
linkStr = linkStr.substr(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [identifier, eventId] = linkStr.split("/");
|
const [identifier, eventId] = linkStr.split("/");
|
||||||
|
|
||||||
let matches;
|
let matches;
|
||||||
matches = USERID_PATTERN.exec(identifier);
|
matches = USERID_PATTERN.exec(identifier);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const server = matches[2];
|
const server = matches[2];
|
||||||
const localPart = matches[1];
|
const localPart = matches[1];
|
||||||
return new Link(clientId, viaServers, IdentifierKind.UserId, localPart, server, webInstances);
|
return new Link(clientId, viaServers, IdentifierKind.UserId, localPart, server, webInstances);
|
||||||
}
|
}
|
||||||
matches = ROOMALIAS_PATTERN.exec(identifier);
|
matches = ROOMALIAS_PATTERN.exec(identifier);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const server = matches[2];
|
const server = matches[2];
|
||||||
const localPart = matches[1];
|
const localPart = matches[1];
|
||||||
return new Link(clientId, viaServers, IdentifierKind.RoomAlias, localPart, server, webInstances, eventId);
|
return new Link(clientId, viaServers, IdentifierKind.RoomAlias, localPart, server, webInstances, eventId);
|
||||||
}
|
}
|
||||||
matches = ROOMID_PATTERN.exec(identifier);
|
matches = ROOMID_PATTERN.exec(identifier);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const server = matches[2];
|
const server = matches[2];
|
||||||
const localPart = matches[1];
|
const localPart = matches[1];
|
||||||
return new Link(clientId, viaServers, IdentifierKind.RoomId, localPart, server, webInstances, eventId);
|
return new Link(clientId, viaServers, IdentifierKind.RoomId, localPart, server, webInstances, eventId);
|
||||||
}
|
}
|
||||||
matches = GROUPID_PATTERN.exec(identifier);
|
matches = GROUPID_PATTERN.exec(identifier);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const server = matches[2];
|
const server = matches[2];
|
||||||
const localPart = matches[1];
|
const localPart = matches[1];
|
||||||
return new Link(clientId, viaServers, IdentifierKind.GroupId, localPart, server, webInstances);
|
return new Link(clientId, viaServers, IdentifierKind.GroupId, localPart, server, webInstances);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(clientId, viaServers, identifierKind, localPart, server, webInstances, eventId) {
|
constructor(clientId, viaServers, identifierKind, localPart, server, webInstances, eventId) {
|
||||||
const servers = [server];
|
const servers = [server];
|
||||||
servers.push(...viaServers);
|
servers.push(...viaServers);
|
||||||
this.webInstances = webInstances;
|
this.webInstances = webInstances;
|
||||||
this.servers = orderedUnique(servers);
|
this.servers = orderedUnique(servers);
|
||||||
this.identifierKind = identifierKind;
|
this.identifierKind = identifierKind;
|
||||||
this.identifier = `${asPrefix(identifierKind)}${localPart}:${server}`;
|
this.identifier = `${asPrefix(identifierKind)}${localPart}:${server}`;
|
||||||
this.eventId = eventId;
|
this.eventId = eventId;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
get kind() {
|
get kind() {
|
||||||
if (this.eventId) {
|
if (this.eventId) {
|
||||||
return LinkKind.Event;
|
return LinkKind.Event;
|
||||||
}
|
}
|
||||||
switch (this.identifierKind) {
|
switch (this.identifierKind) {
|
||||||
case IdentifierKind.RoomId:
|
case IdentifierKind.RoomId:
|
||||||
case IdentifierKind.RoomAlias:
|
case IdentifierKind.RoomAlias:
|
||||||
return LinkKind.Room;
|
return LinkKind.Room;
|
||||||
case IdentifierKind.UserId:
|
case IdentifierKind.UserId:
|
||||||
return LinkKind.User;
|
return LinkKind.User;
|
||||||
case IdentifierKind.GroupId:
|
case IdentifierKind.GroupId:
|
||||||
return LinkKind.Group;
|
return LinkKind.Group;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
equals(link) {
|
equals(link) {
|
||||||
return link &&
|
return link &&
|
||||||
link.identifier === this.identifier &&
|
link.identifier === this.identifier &&
|
||||||
this.servers.length === link.servers.length &&
|
this.servers.length === link.servers.length &&
|
||||||
this.servers.every((s, i) => link.servers[i] === s) &&
|
this.servers.every((s, i) => link.servers[i] === s) &&
|
||||||
Object.keys(this.webInstances).length === Object.keys(link.webInstances).length &&
|
Object.keys(this.webInstances).length === Object.keys(link.webInstances).length &&
|
||||||
Object.keys(this.webInstances).every(k => this.webInstances[k] === link.webInstances[k]);
|
Object.keys(this.webInstances).every(k => this.webInstances[k] === link.webInstances[k]);
|
||||||
}
|
}
|
||||||
|
|
||||||
toFragment() {
|
toFragment() {
|
||||||
if (this.eventId) {
|
if (this.eventId) {
|
||||||
return `/${this.identifier}/${this.eventId}`;
|
return `/${this.identifier}/${this.eventId}`;
|
||||||
} else {
|
} else {
|
||||||
return `/${this.identifier}`;
|
return `/${this.identifier}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,17 +17,17 @@ limitations under the License.
|
||||||
import {createEnum} from "./utils/enum.js";
|
import {createEnum} from "./utils/enum.js";
|
||||||
|
|
||||||
export const Platform = createEnum(
|
export const Platform = createEnum(
|
||||||
"DesktopWeb",
|
"DesktopWeb",
|
||||||
"MobileWeb",
|
"MobileWeb",
|
||||||
"Android",
|
"Android",
|
||||||
"iOS",
|
"iOS",
|
||||||
"Windows",
|
"Windows",
|
||||||
"macOS",
|
"macOS",
|
||||||
"Linux"
|
"Linux"
|
||||||
);
|
);
|
||||||
|
|
||||||
export function guessApplicablePlatforms(userAgent, platform) {
|
export function guessApplicablePlatforms(userAgent, platform) {
|
||||||
// return [Platform.DesktopWeb, Platform.Linux];
|
// return [Platform.DesktopWeb, Platform.Linux];
|
||||||
let nativePlatform;
|
let nativePlatform;
|
||||||
let webPlatform;
|
let webPlatform;
|
||||||
if (/android/i.test(userAgent)) {
|
if (/android/i.test(userAgent)) {
|
||||||
|
@ -55,10 +55,10 @@ export function guessApplicablePlatforms(userAgent, platform) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isWebPlatform(p) {
|
export function isWebPlatform(p) {
|
||||||
return p === Platform.DesktopWeb || p === Platform.MobileWeb;
|
return p === Platform.DesktopWeb || p === Platform.MobileWeb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function isDesktopPlatform(p) {
|
export function isDesktopPlatform(p) {
|
||||||
return p === Platform.Linux || p === Platform.Windows || p === Platform.macOS;
|
return p === Platform.Linux || p === Platform.Windows || p === Platform.macOS;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,51 +18,51 @@ import {Platform} from "./Platform.js";
|
||||||
import {EventEmitter} from "./utils/ViewModel.js";
|
import {EventEmitter} from "./utils/ViewModel.js";
|
||||||
|
|
||||||
export class Preferences extends EventEmitter {
|
export class Preferences extends EventEmitter {
|
||||||
constructor(localStorage) {
|
constructor(localStorage) {
|
||||||
super();
|
super();
|
||||||
this._localStorage = localStorage;
|
this._localStorage = localStorage;
|
||||||
this.clientId = null;
|
this.clientId = null;
|
||||||
// used to differentiate web from native if a client supports both
|
// used to differentiate web from native if a client supports both
|
||||||
this.platform = null;
|
this.platform = null;
|
||||||
this.homeservers = null;
|
this.homeservers = null;
|
||||||
|
|
||||||
const prefsStr = localStorage.getItem("preferred_client");
|
const prefsStr = localStorage.getItem("preferred_client");
|
||||||
if (prefsStr) {
|
if (prefsStr) {
|
||||||
const {id, platform} = JSON.parse(prefsStr);
|
const {id, platform} = JSON.parse(prefsStr);
|
||||||
this.clientId = id;
|
this.clientId = id;
|
||||||
this.platform = Platform[platform];
|
this.platform = Platform[platform];
|
||||||
}
|
}
|
||||||
const serversStr = localStorage.getItem("consented_servers");
|
const serversStr = localStorage.getItem("consented_servers");
|
||||||
if (serversStr) {
|
if (serversStr) {
|
||||||
this.homeservers = JSON.parse(serversStr);
|
this.homeservers = JSON.parse(serversStr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setClient(id, platform) {
|
setClient(id, platform) {
|
||||||
this.clientId = id;
|
this.clientId = id;
|
||||||
platform = Platform[platform];
|
platform = Platform[platform];
|
||||||
this.platform = platform;
|
this.platform = platform;
|
||||||
this._localStorage.setItem("preferred_client", JSON.stringify({id, platform}));
|
this._localStorage.setItem("preferred_client", JSON.stringify({id, platform}));
|
||||||
this.emit("canClear")
|
this.emit("canClear")
|
||||||
}
|
}
|
||||||
|
|
||||||
setHomeservers(homeservers, persist) {
|
setHomeservers(homeservers, persist) {
|
||||||
this.homeservers = homeservers;
|
this.homeservers = homeservers;
|
||||||
if (persist) {
|
if (persist) {
|
||||||
this._localStorage.setItem("consented_servers", JSON.stringify(homeservers));
|
this._localStorage.setItem("consented_servers", JSON.stringify(homeservers));
|
||||||
this.emit("canClear");
|
this.emit("canClear");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this._localStorage.removeItem("preferred_client");
|
this._localStorage.removeItem("preferred_client");
|
||||||
this._localStorage.removeItem("consented_servers");
|
this._localStorage.removeItem("consented_servers");
|
||||||
this.clientId = null;
|
this.clientId = null;
|
||||||
this.platform = null;
|
this.platform = null;
|
||||||
this.homeservers = null;
|
this.homeservers = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canClear() {
|
get canClear() {
|
||||||
return !!this.clientId || !!this.platform || !!this.homeservers;
|
return !!this.clientId || !!this.platform || !!this.homeservers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,25 +20,25 @@ import {CreateLinkView} from "./create/CreateLinkView.js";
|
||||||
import {LoadServerPolicyView} from "./policy/LoadServerPolicyView.js";
|
import {LoadServerPolicyView} from "./policy/LoadServerPolicyView.js";
|
||||||
|
|
||||||
export class RootView extends TemplateView {
|
export class RootView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.div({className: "RootView"}, [
|
return t.div({className: "RootView"}, [
|
||||||
t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null),
|
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.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null),
|
||||||
t.mapView(vm => vm.loadServerPolicyViewModel, vm => vm ? new LoadServerPolicyView(vm) : null),
|
t.mapView(vm => vm.loadServerPolicyViewModel, vm => vm ? new LoadServerPolicyView(vm) : null),
|
||||||
t.div({className: "footer"}, [
|
t.div({className: "footer"}, [
|
||||||
t.p(t.img({src: "images/matrix-logo.svg"})),
|
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.p(["This invite uses ", externalLink(t, "https://matrix.org", "Matrix"), ", an open network for secure, decentralized communication."]),
|
||||||
t.ul({className: "links"}, [
|
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", "GitHub project")),
|
||||||
t.li(externalLink(t, "https://github.com/matrix-org/matrix.to/tree/main/src/open/clients", "Add your app")),
|
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.li({className: {hidden: vm => !vm.hasPreferences}},
|
||||||
t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")),
|
t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")),
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function externalLink(t, href, label) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,51 +23,51 @@ import {LoadServerPolicyViewModel} from "./policy/LoadServerPolicyViewModel.js";
|
||||||
import {Platform} from "./Platform.js";
|
import {Platform} from "./Platform.js";
|
||||||
|
|
||||||
export class RootViewModel extends ViewModel {
|
export class RootViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this.link = null;
|
this.link = null;
|
||||||
this.openLinkViewModel = null;
|
this.openLinkViewModel = null;
|
||||||
this.createLinkViewModel = null;
|
this.createLinkViewModel = null;
|
||||||
this.loadServerPolicyViewModel = null;
|
this.loadServerPolicyViewModel = null;
|
||||||
this.preferences.on("canClear", () => {
|
this.preferences.on("canClear", () => {
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateChildVMs(oldLink) {
|
_updateChildVMs(oldLink) {
|
||||||
if (this.link) {
|
if (this.link) {
|
||||||
this.createLinkViewModel = null;
|
this.createLinkViewModel = null;
|
||||||
if (!oldLink || !oldLink.equals(this.link)) {
|
if (!oldLink || !oldLink.equals(this.link)) {
|
||||||
this.openLinkViewModel = new OpenLinkViewModel(this.childOptions({
|
this.openLinkViewModel = new OpenLinkViewModel(this.childOptions({
|
||||||
link: this.link,
|
link: this.link,
|
||||||
clients: createClients(),
|
clients: createClients(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.openLinkViewModel = null;
|
this.openLinkViewModel = null;
|
||||||
this.createLinkViewModel = new CreateLinkViewModel(this.childOptions());
|
this.createLinkViewModel = new CreateLinkViewModel(this.childOptions());
|
||||||
}
|
}
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHash(hash) {
|
updateHash(hash) {
|
||||||
if (hash.startsWith("#/policy/")) {
|
if (hash.startsWith("#/policy/")) {
|
||||||
const server = hash.substr(9);
|
const server = hash.substr(9);
|
||||||
this.loadServerPolicyViewModel = new LoadServerPolicyViewModel(this.childOptions({server}));
|
this.loadServerPolicyViewModel = new LoadServerPolicyViewModel(this.childOptions({server}));
|
||||||
this.loadServerPolicyViewModel.load();
|
this.loadServerPolicyViewModel.load();
|
||||||
} else {
|
} else {
|
||||||
const oldLink = this.link;
|
const oldLink = this.link;
|
||||||
this.link = Link.parse(hash);
|
this.link = Link.parse(hash);
|
||||||
this._updateChildVMs(oldLink);
|
this._updateChildVMs(oldLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPreferences() {
|
clearPreferences() {
|
||||||
this.preferences.clear();
|
this.preferences.clear();
|
||||||
this._updateChildVMs();
|
this._updateChildVMs();
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasPreferences() {
|
get hasPreferences() {
|
||||||
return this.preferences.canClear;
|
return this.preferences.canClear;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,31 +19,31 @@ import {PreviewView} from "../preview/PreviewView.js";
|
||||||
import {copyButton} from "../utils/copy.js";
|
import {copyButton} from "../utils/copy.js";
|
||||||
|
|
||||||
export class CreateLinkView extends TemplateView {
|
export class CreateLinkView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
const link = t.a({href: vm => vm.linkUrl}, vm => vm.linkUrl);
|
const link = t.a({href: vm => vm.linkUrl}, vm => vm.linkUrl);
|
||||||
return t.div({className: "CreateLinkView card"}, [
|
return t.div({className: "CreateLinkView card"}, [
|
||||||
t.h1("Create shareable links to Matrix rooms, users or messages without being tied to any app"),
|
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.form({action: "#", onSubmit: evt => this._onSubmit(evt)}, [
|
||||||
t.div(t.input({
|
t.div(t.input({
|
||||||
className: "fullwidth large",
|
className: "fullwidth large",
|
||||||
type: "text",
|
type: "text",
|
||||||
name: "identifier",
|
name: "identifier",
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: "#room:example.com, @user:example.com",
|
placeholder: "#room:example.com, @user:example.com",
|
||||||
onChange: evt => this._onIdentifierChange(evt)
|
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) {
|
_onSubmit(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const form = evt.target;
|
const form = evt.target;
|
||||||
const {identifier} = form.elements;
|
const {identifier} = form.elements;
|
||||||
this.value.createLink(identifier.value);
|
this.value.createLink(identifier.value);
|
||||||
identifier.value = "";
|
identifier.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
_onIdentifierChange(evt) {
|
_onIdentifierChange(evt) {
|
||||||
const inputField = evt.target;
|
const inputField = evt.target;
|
||||||
|
|
|
@ -19,11 +19,11 @@ import {PreviewViewModel} from "../preview/PreviewViewModel.js";
|
||||||
import {Link} from "../Link.js";
|
import {Link} from "../Link.js";
|
||||||
|
|
||||||
export class CreateLinkViewModel extends ViewModel {
|
export class CreateLinkViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this._link = null;
|
this._link = null;
|
||||||
this.previewViewModel = null;
|
this.previewViewModel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
validateIdentifier(identifier) {
|
validateIdentifier(identifier) {
|
||||||
return Link.validateIdentifier(identifier);
|
return Link.validateIdentifier(identifier);
|
||||||
|
|
28
src/main.js
28
src/main.js
|
@ -21,18 +21,18 @@ import {Preferences} from "./Preferences.js";
|
||||||
import {guessApplicablePlatforms} from "./Platform.js";
|
import {guessApplicablePlatforms} from "./Platform.js";
|
||||||
|
|
||||||
export async function main(container) {
|
export async function main(container) {
|
||||||
const vm = new RootViewModel({
|
const vm = new RootViewModel({
|
||||||
request: xhrRequest,
|
request: xhrRequest,
|
||||||
openLink: url => location.href = url,
|
openLink: url => location.href = url,
|
||||||
platforms: guessApplicablePlatforms(navigator.userAgent, navigator.platform),
|
platforms: guessApplicablePlatforms(navigator.userAgent, navigator.platform),
|
||||||
preferences: new Preferences(window.localStorage),
|
preferences: new Preferences(window.localStorage),
|
||||||
origin: location.origin,
|
origin: location.origin,
|
||||||
});
|
});
|
||||||
vm.updateHash(decodeURIComponent(location.hash));
|
vm.updateHash(decodeURIComponent(location.hash));
|
||||||
window.__rootvm = vm;
|
window.__rootvm = vm;
|
||||||
const view = new RootView(vm);
|
const view = new RootView(vm);
|
||||||
container.appendChild(view.mount());
|
container.appendChild(view.mount());
|
||||||
window.addEventListener('hashchange', () => {
|
window.addEventListener('hashchange', () => {
|
||||||
vm.updateHash(decodeURIComponent(location.hash));
|
vm.updateHash(decodeURIComponent(location.hash));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,50 +18,50 @@ import {TemplateView} from "../utils/TemplateView.js";
|
||||||
import {ClientView} from "./ClientView.js";
|
import {ClientView} from "./ClientView.js";
|
||||||
|
|
||||||
export class ClientListView extends TemplateView {
|
export class ClientListView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.mapView(vm => vm.clientViewModel, () => {
|
return t.mapView(vm => vm.clientViewModel, () => {
|
||||||
if (vm.clientViewModel) {
|
if (vm.clientViewModel) {
|
||||||
return new ContinueWithClientView(vm);
|
return new ContinueWithClientView(vm);
|
||||||
} else {
|
} else {
|
||||||
return new AllClientsView(vm);
|
return new AllClientsView(vm);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AllClientsView extends TemplateView {
|
class AllClientsView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.div({className: "ClientListView"}, [
|
return t.div({className: "ClientListView"}, [
|
||||||
t.h2("Choose an app to continue"),
|
t.h2("Choose an app to continue"),
|
||||||
t.map(vm => vm.clientList, (clientList, t) => {
|
t.map(vm => vm.clientList, (clientList, t) => {
|
||||||
return t.div({className: "list"}, clientList.map(clientViewModel => {
|
return t.div({className: "list"}, clientList.map(clientViewModel => {
|
||||||
return t.view(new ClientView(clientViewModel));
|
return t.view(new ClientView(clientViewModel));
|
||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
t.div(t.label([
|
t.div(t.label([
|
||||||
t.input({
|
t.input({
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: vm.showUnsupportedPlatforms,
|
checked: vm.showUnsupportedPlatforms,
|
||||||
onChange: evt => vm.showUnsupportedPlatforms = evt.target.checked,
|
onChange: evt => vm.showUnsupportedPlatforms = evt.target.checked,
|
||||||
}),
|
}),
|
||||||
"Show apps not available on my platform"
|
"Show apps not available on my platform"
|
||||||
])),
|
])),
|
||||||
t.div(t.label({className: "filterOption"}, [
|
t.div(t.label({className: "filterOption"}, [
|
||||||
t.input({
|
t.input({
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: vm.showExperimental,
|
checked: vm.showExperimental,
|
||||||
onChange: evt => vm.showExperimental = evt.target.checked,
|
onChange: evt => vm.showExperimental = evt.target.checked,
|
||||||
}),
|
}),
|
||||||
"Show experimental apps"
|
"Show experimental apps"
|
||||||
])),
|
])),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContinueWithClientView extends TemplateView {
|
class ContinueWithClientView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.div({className: "ClientListView"}, [
|
return t.div({className: "ClientListView"}, [
|
||||||
t.div({className: "list"}, t.view(new ClientView(vm.clientViewModel)))
|
t.div({className: "list"}, t.view(new ClientView(vm.clientViewModel)))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,70 +20,70 @@ import {ClientViewModel} from "./ClientViewModel.js";
|
||||||
import {ViewModel} from "../utils/ViewModel.js";
|
import {ViewModel} from "../utils/ViewModel.js";
|
||||||
|
|
||||||
export class ClientListViewModel extends ViewModel {
|
export class ClientListViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
const {clients, client, link} = options;
|
const {clients, client, link} = options;
|
||||||
this._clients = clients;
|
this._clients = clients;
|
||||||
this._link = link;
|
this._link = link;
|
||||||
this.clientList = null;
|
this.clientList = null;
|
||||||
this._showExperimental = false;
|
this._showExperimental = false;
|
||||||
this._showUnsupportedPlatforms = false;
|
this._showUnsupportedPlatforms = false;
|
||||||
this._filterClients();
|
this._filterClients();
|
||||||
this.clientViewModel = null;
|
this.clientViewModel = null;
|
||||||
if (client) {
|
if (client) {
|
||||||
this._pickClient(client);
|
this._pickClient(client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get showUnsupportedPlatforms() {
|
get showUnsupportedPlatforms() {
|
||||||
return this._showUnsupportedPlatforms;
|
return this._showUnsupportedPlatforms;
|
||||||
}
|
}
|
||||||
|
|
||||||
get showExperimental() {
|
get showExperimental() {
|
||||||
return this._showExperimental;
|
return this._showExperimental;
|
||||||
}
|
}
|
||||||
|
|
||||||
set showUnsupportedPlatforms(enabled) {
|
set showUnsupportedPlatforms(enabled) {
|
||||||
this._showUnsupportedPlatforms = enabled;
|
this._showUnsupportedPlatforms = enabled;
|
||||||
this._filterClients();
|
this._filterClients();
|
||||||
}
|
}
|
||||||
|
|
||||||
set showExperimental(enabled) {
|
set showExperimental(enabled) {
|
||||||
this._showExperimental = enabled;
|
this._showExperimental = enabled;
|
||||||
this._filterClients();
|
this._filterClients();
|
||||||
}
|
}
|
||||||
|
|
||||||
_filterClients() {
|
_filterClients() {
|
||||||
const clientVMs = this._clients.filter(client => {
|
const clientVMs = this._clients.filter(client => {
|
||||||
const platformMaturities = this.platforms.map(p => client.getMaturity(p));
|
const platformMaturities = this.platforms.map(p => client.getMaturity(p));
|
||||||
const isStable = platformMaturities.includes(Maturity.Stable) || platformMaturities.includes(Maturity.Beta);
|
const isStable = platformMaturities.includes(Maturity.Stable) || platformMaturities.includes(Maturity.Beta);
|
||||||
const isSupported = client.platforms.some(p => this.platforms.includes(p));
|
const isSupported = client.platforms.some(p => this.platforms.includes(p));
|
||||||
if (!this._showExperimental && !isStable) {
|
if (!this._showExperimental && !isStable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this._showUnsupportedPlatforms && !isSupported) {
|
if (!this._showUnsupportedPlatforms && !isSupported) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).map(client => new ClientViewModel(this.childOptions({
|
}).map(client => new ClientViewModel(this.childOptions({
|
||||||
client,
|
client,
|
||||||
link: this._link,
|
link: this._link,
|
||||||
pickClient: client => this._pickClient(client)
|
pickClient: client => this._pickClient(client)
|
||||||
})));
|
})));
|
||||||
const preferredClientVMs = clientVMs.filter(c => c.hasPreferredWebInstance);
|
const preferredClientVMs = clientVMs.filter(c => c.hasPreferredWebInstance);
|
||||||
const otherClientVMs = clientVMs.filter(c => !c.hasPreferredWebInstance);
|
const otherClientVMs = clientVMs.filter(c => !c.hasPreferredWebInstance);
|
||||||
this.clientList = preferredClientVMs.concat(otherClientVMs);
|
this.clientList = preferredClientVMs.concat(otherClientVMs);
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
_pickClient(client) {
|
_pickClient(client) {
|
||||||
this.clientViewModel = this.clientList.find(vm => vm.clientId === client.id);
|
this.clientViewModel = this.clientList.find(vm => vm.clientId === client.id);
|
||||||
this.clientViewModel.pick(this);
|
this.clientViewModel.pick(this);
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
showAll() {
|
showAll() {
|
||||||
this.clientViewModel = null;
|
this.clientViewModel = null;
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,11 @@ import {copy} from "../utils/copy.js";
|
||||||
import {text, tag} from "../utils/html.js";
|
import {text, tag} from "../utils/html.js";
|
||||||
|
|
||||||
function formatPlatforms(platforms) {
|
function formatPlatforms(platforms) {
|
||||||
return platforms.reduce((str, p, i, all) => {
|
return platforms.reduce((str, p, i, all) => {
|
||||||
const first = i === 0;
|
const first = i === 0;
|
||||||
const last = i === all.length - 1;
|
const last = i === all.length - 1;
|
||||||
return str + (first ? "" : last ? " & " : ", ") + p;
|
return str + (first ? "" : last ? " & " : ", ") + p;
|
||||||
}, "");
|
}, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderInstructions(parts) {
|
function renderInstructions(parts) {
|
||||||
|
@ -38,46 +38,46 @@ function renderInstructions(parts) {
|
||||||
|
|
||||||
export class ClientView extends TemplateView {
|
export class ClientView extends TemplateView {
|
||||||
|
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.div({className: {"ClientView": true, "isPreferred": vm => vm.hasPreferredWebInstance}}, [
|
return t.div({className: {"ClientView": true, "isPreferred": vm => vm.hasPreferredWebInstance}}, [
|
||||||
... vm.hasPreferredWebInstance ? [t.div({className: "hostedBanner"}, vm.hostedByBannerLabel)] : [],
|
... vm.hasPreferredWebInstance ? [t.div({className: "hostedBanner"}, vm.hostedByBannerLabel)] : [],
|
||||||
t.div({className: "header"}, [
|
t.div({className: "header"}, [
|
||||||
t.div({className: "description"}, [
|
t.div({className: "description"}, [
|
||||||
t.h3(vm.name),
|
t.h3(vm.name),
|
||||||
t.p([vm.description, " ", t.a({
|
t.p([vm.description, " ", t.a({
|
||||||
href: vm.homepage,
|
href: vm.homepage,
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
rel: "noopener noreferrer"
|
rel: "noopener noreferrer"
|
||||||
}, "Learn more")]),
|
}, "Learn more")]),
|
||||||
t.p({className: "platforms"}, formatPlatforms(vm.availableOnPlatformNames)),
|
t.p({className: "platforms"}, formatPlatforms(vm.availableOnPlatformNames)),
|
||||||
]),
|
]),
|
||||||
t.img({className: "clientIcon", src: vm.iconUrl})
|
t.img({className: "clientIcon", src: vm.iconUrl})
|
||||||
]),
|
]),
|
||||||
t.mapView(vm => vm.stage, stage => {
|
t.mapView(vm => vm.stage, stage => {
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case "open": return new OpenClientView(vm);
|
case "open": return new OpenClientView(vm);
|
||||||
case "install": return new InstallClientView(vm);
|
case "install": return new InstallClientView(vm);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OpenClientView extends TemplateView {
|
class OpenClientView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.div({className: "OpenClientView"}, [
|
return t.div({className: "OpenClientView"}, [
|
||||||
...vm.openActions.map(a => renderAction(t, a)),
|
...vm.openActions.map(a => renderAction(t, a)),
|
||||||
showBack(t, vm),
|
showBack(t, vm),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InstallClientView extends TemplateView {
|
class InstallClientView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
const children = [];
|
const children = [];
|
||||||
|
|
||||||
const textInstructions = vm.textInstructions;
|
const textInstructions = vm.textInstructions;
|
||||||
if (textInstructions) {
|
if (textInstructions) {
|
||||||
const copyButton = t.button({
|
const copyButton = t.button({
|
||||||
className: "copy",
|
className: "copy",
|
||||||
title: "Copy instructions",
|
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)));
|
const actions = t.div({className: "actions"}, vm.installActions.map(a => renderAction(t, a)));
|
||||||
children.push(actions);
|
children.push(actions);
|
||||||
|
|
||||||
if (vm.showDeepLinkInInstall) {
|
if (vm.showDeepLinkInInstall) {
|
||||||
const openItHere = t.a({
|
const openItHere = t.a({
|
||||||
rel: "noopener noreferrer",
|
rel: "noopener noreferrer",
|
||||||
href: vm.openActions[0].url,
|
href: vm.openActions[0].url,
|
||||||
onClick: () => vm.openActions[0].activated(),
|
onClick: () => vm.openActions[0].activated(),
|
||||||
}, "open it here");
|
}, "open it here");
|
||||||
children.push(t.p([`If you already have ${vm.name} installed, you can `, openItHere, "."]))
|
children.push(t.p([`If you already have ${vm.name} installed, you can `, openItHere, "."]))
|
||||||
}
|
}
|
||||||
|
|
||||||
children.push(showBack(t, vm));
|
children.push(showBack(t, vm));
|
||||||
|
|
||||||
return t.div({className: "InstallClientView"}, children);
|
return t.div({className: "InstallClientView"}, children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showBack(t, vm) {
|
function showBack(t, vm) {
|
||||||
|
|
|
@ -27,28 +27,28 @@ function getMatchingPlatforms(client, supportedPlatforms) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ClientViewModel extends ViewModel {
|
export class ClientViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
const {client, link, pickClient} = options;
|
const {client, link, pickClient} = options;
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._link = link;
|
this._link = link;
|
||||||
this._pickClient = pickClient;
|
this._pickClient = pickClient;
|
||||||
// to provide "choose other client" button after calling pick()
|
// to provide "choose other client" button after calling pick()
|
||||||
this._clientListViewModel = null;
|
this._clientListViewModel = null;
|
||||||
this._update();
|
this._update();
|
||||||
}
|
}
|
||||||
|
|
||||||
_update() {
|
_update() {
|
||||||
const matchingPlatforms = getMatchingPlatforms(this._client, this.platforms);
|
const matchingPlatforms = getMatchingPlatforms(this._client, this.platforms);
|
||||||
this._webPlatform = matchingPlatforms.find(p => isWebPlatform(p));
|
this._webPlatform = matchingPlatforms.find(p => isWebPlatform(p));
|
||||||
this._nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p));
|
this._nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p));
|
||||||
const preferredPlatform = matchingPlatforms.find(p => p === this.preferences.platform);
|
const preferredPlatform = matchingPlatforms.find(p => p === this.preferences.platform);
|
||||||
this._proposedPlatform = preferredPlatform || this._nativePlatform || this._webPlatform;
|
this._proposedPlatform = preferredPlatform || this._nativePlatform || this._webPlatform;
|
||||||
|
|
||||||
this.openActions = this._createOpenActions();
|
this.openActions = this._createOpenActions();
|
||||||
this.installActions = this._createInstallActions();
|
this.installActions = this._createInstallActions();
|
||||||
this._clientCanIntercept = !!(this._nativePlatform && this._client.canInterceptMatrixToLinks(this._nativePlatform));
|
this._clientCanIntercept = !!(this._nativePlatform && this._client.canInterceptMatrixToLinks(this._nativePlatform));
|
||||||
this._showOpen = this.openActions.length && !this._clientCanIntercept;
|
this._showOpen = this.openActions.length && !this._clientCanIntercept;
|
||||||
}
|
}
|
||||||
|
|
||||||
// these are only shown in the open stage
|
// 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
|
// these are only shown in the install stage
|
||||||
_createInstallActions() {
|
_createInstallActions() {
|
||||||
let actions = [];
|
let actions = [];
|
||||||
if (this._nativePlatform) {
|
if (this._nativePlatform) {
|
||||||
const nativeActions = (this._client.getInstallLinks(this._nativePlatform) || []).map(installLink => {
|
const nativeActions = (this._client.getInstallLinks(this._nativePlatform) || []).map(installLink => {
|
||||||
return {
|
return {
|
||||||
label: installLink.getDescription(this._nativePlatform),
|
label: installLink.getDescription(this._nativePlatform),
|
||||||
url: installLink.createInstallURL(this._link),
|
url: installLink.createInstallURL(this._link),
|
||||||
kind: installLink.channelId,
|
kind: installLink.channelId,
|
||||||
primary: true,
|
primary: true,
|
||||||
activated: () => this.preferences.setClient(this._client.id, this._nativePlatform),
|
activated: () => this.preferences.setClient(this._client.id, this._nativePlatform),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
actions.push(...nativeActions);
|
actions.push(...nativeActions);
|
||||||
}
|
}
|
||||||
if (this._webPlatform) {
|
if (this._webPlatform) {
|
||||||
const webDeepLink = this._client.getDeepLink(this._webPlatform, this._link);
|
const webDeepLink = this._client.getDeepLink(this._webPlatform, this._link);
|
||||||
if (webDeepLink) {
|
if (webDeepLink) {
|
||||||
const webLabel = this.hasPreferredWebInstance ?
|
const webLabel = this.hasPreferredWebInstance ?
|
||||||
`Open on ${this._client.getPreferredWebInstance(this._link)}` :
|
`Open on ${this._client.getPreferredWebInstance(this._link)}` :
|
||||||
`Continue in your browser`;
|
`Continue in your browser`;
|
||||||
actions.push({
|
actions.push({
|
||||||
label: webLabel,
|
label: webLabel,
|
||||||
url: webDeepLink,
|
url: webDeepLink,
|
||||||
kind: "open-in-web",
|
kind: "open-in-web",
|
||||||
activated: () => {
|
activated: () => {
|
||||||
if (!this.hasPreferredWebInstance) {
|
if (!this.hasPreferredWebInstance) {
|
||||||
this.preferences.setClient(this._client.id, this._webPlatform);
|
this.preferences.setClient(this._client.id, this._webPlatform);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasPreferredWebInstance() {
|
get hasPreferredWebInstance() {
|
||||||
// also check there is a web platform that matches the platforms the user is on (mobile or desktop web)
|
// 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;
|
return this._client.homepage;
|
||||||
}
|
}
|
||||||
|
|
||||||
get identifier() {
|
get identifier() {
|
||||||
return this._link.identifier;
|
return this._link.identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
get description() {
|
get description() {
|
||||||
return this._client.description;
|
return this._client.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
get clientId() {
|
get clientId() {
|
||||||
return this._client.id;
|
return this._client.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return this._client.name;
|
return this._client.name;
|
||||||
|
@ -174,44 +174,44 @@ export class ClientViewModel extends ViewModel {
|
||||||
return this._showOpen ? "open" : "install";
|
return this._showOpen ? "open" : "install";
|
||||||
}
|
}
|
||||||
|
|
||||||
get textInstructions() {
|
get textInstructions() {
|
||||||
let instructions = this._client.getLinkInstructions(this._proposedPlatform, this._link);
|
let instructions = this._client.getLinkInstructions(this._proposedPlatform, this._link);
|
||||||
if (instructions && !Array.isArray(instructions)) {
|
if (instructions && !Array.isArray(instructions)) {
|
||||||
instructions = [instructions];
|
instructions = [instructions];
|
||||||
}
|
}
|
||||||
return instructions;
|
return instructions;
|
||||||
}
|
}
|
||||||
|
|
||||||
get copyString() {
|
get copyString() {
|
||||||
return this._client.getCopyString(this._proposedPlatform, this._link);
|
return this._client.getCopyString(this._proposedPlatform, this._link);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showDeepLinkInInstall() {
|
get showDeepLinkInInstall() {
|
||||||
// we can assume this._nativePlatform as this._clientCanIntercept already checks it
|
// 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() {
|
get availableOnPlatformNames() {
|
||||||
const platforms = this._client.platforms;
|
const platforms = this._client.platforms;
|
||||||
const textPlatforms = [];
|
const textPlatforms = [];
|
||||||
const hasWebPlatform = platforms.some(p => isWebPlatform(p));
|
const hasWebPlatform = platforms.some(p => isWebPlatform(p));
|
||||||
if (hasWebPlatform) {
|
if (hasWebPlatform) {
|
||||||
textPlatforms.push("Web");
|
textPlatforms.push("Web");
|
||||||
}
|
}
|
||||||
const desktopPlatforms = platforms.filter(p => isDesktopPlatform(p));
|
const desktopPlatforms = platforms.filter(p => isDesktopPlatform(p));
|
||||||
if (desktopPlatforms.length === 1) {
|
if (desktopPlatforms.length === 1) {
|
||||||
textPlatforms.push(desktopPlatforms[0]);
|
textPlatforms.push(desktopPlatforms[0]);
|
||||||
} else {
|
} else {
|
||||||
textPlatforms.push("Desktop");
|
textPlatforms.push("Desktop");
|
||||||
}
|
}
|
||||||
if (platforms.includes(Platform.Android)) {
|
if (platforms.includes(Platform.Android)) {
|
||||||
textPlatforms.push("Android");
|
textPlatforms.push("Android");
|
||||||
}
|
}
|
||||||
if (platforms.includes(Platform.iOS)) {
|
if (platforms.includes(Platform.iOS)) {
|
||||||
textPlatforms.push("iOS");
|
textPlatforms.push("iOS");
|
||||||
}
|
}
|
||||||
return textPlatforms;
|
return textPlatforms;
|
||||||
}
|
}
|
||||||
|
|
||||||
pick(clientListViewModel) {
|
pick(clientListViewModel) {
|
||||||
this._clientListViewModel = clientListViewModel;
|
this._clientListViewModel = clientListViewModel;
|
||||||
|
|
|
@ -20,14 +20,14 @@ import {PreviewView} from "../preview/PreviewView.js";
|
||||||
import {ServerConsentView} from "./ServerConsentView.js";
|
import {ServerConsentView} from "./ServerConsentView.js";
|
||||||
|
|
||||||
export class OpenLinkView extends TemplateView {
|
export class OpenLinkView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.div({className: "OpenLinkView card"}, [
|
return t.div({className: "OpenLinkView card"}, [
|
||||||
t.mapView(vm => vm.previewViewModel, previewVM => previewVM ?
|
t.mapView(vm => vm.previewViewModel, previewVM => previewVM ?
|
||||||
new ShowLinkView(vm) :
|
new ShowLinkView(vm) :
|
||||||
new ServerConsentView(vm.serverConsentViewModel)
|
new ServerConsentView(vm.serverConsentViewModel)
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShowLinkView extends TemplateView {
|
class ShowLinkView extends TemplateView {
|
||||||
|
|
|
@ -23,21 +23,21 @@ import {getLabelForLinkKind} from "../Link.js";
|
||||||
import {orderedUnique} from "../utils/unique.js";
|
import {orderedUnique} from "../utils/unique.js";
|
||||||
|
|
||||||
export class OpenLinkViewModel extends ViewModel {
|
export class OpenLinkViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
const {clients, link} = options;
|
const {clients, link} = options;
|
||||||
this._link = link;
|
this._link = link;
|
||||||
this._clients = clients;
|
this._clients = clients;
|
||||||
this.serverConsentViewModel = null;
|
this.serverConsentViewModel = null;
|
||||||
this.previewViewModel = null;
|
this.previewViewModel = null;
|
||||||
this.clientsViewModel = null;
|
this.clientsViewModel = null;
|
||||||
this.previewLoading = false;
|
this.previewLoading = false;
|
||||||
if (this.preferences.homeservers === null) {
|
if (this.preferences.homeservers === null) {
|
||||||
this._showServerConsent();
|
this._showServerConsent();
|
||||||
} else {
|
} else {
|
||||||
this._showLink();
|
this._showLink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_showServerConsent() {
|
_showServerConsent() {
|
||||||
let servers = [];
|
let servers = [];
|
||||||
|
@ -67,24 +67,24 @@ export class OpenLinkViewModel extends ViewModel {
|
||||||
link: this._link,
|
link: this._link,
|
||||||
consentedServers: this.preferences.homeservers
|
consentedServers: this.preferences.homeservers
|
||||||
}));
|
}));
|
||||||
this.previewLoading = true;
|
this.previewLoading = true;
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
await this.previewViewModel.load();
|
await this.previewViewModel.load();
|
||||||
this.previewLoading = false;
|
this.previewLoading = false;
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
get previewDomain() {
|
get previewDomain() {
|
||||||
return this.previewViewModel?.domain;
|
return this.previewViewModel?.domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
get previewFailed() {
|
get previewFailed() {
|
||||||
return this.previewViewModel?.failed;
|
return this.previewViewModel?.failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
get showClientsLabel() {
|
get showClientsLabel() {
|
||||||
return getLabelForLinkKind(this._link.kind);
|
return getLabelForLinkKind(this._link.kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeServer() {
|
changeServer() {
|
||||||
this.previewViewModel = null;
|
this.previewViewModel = null;
|
||||||
|
|
|
@ -27,7 +27,7 @@ export class ServerConsentView extends TemplateView {
|
||||||
className: "text",
|
className: "text",
|
||||||
onClick: () => vm.continueWithoutConsent(this._askEveryTimeChecked)
|
onClick: () => vm.continueWithoutConsent(this._askEveryTimeChecked)
|
||||||
}, "continue without a preview");
|
}, "continue without a preview");
|
||||||
return t.div({className: "ServerConsentView"}, [
|
return t.div({className: "ServerConsentView"}, [
|
||||||
t.p([
|
t.p([
|
||||||
"Preview this link using the ",
|
"Preview this link using the ",
|
||||||
t.strong(vm => vm.selectedServer || "…"),
|
t.strong(vm => vm.selectedServer || "…"),
|
||||||
|
@ -56,7 +56,7 @@ export class ServerConsentView extends TemplateView {
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSubmit(evt) {
|
_onSubmit(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
|
@ -22,13 +22,13 @@ import {getLabelForLinkKind} from "../Link.js";
|
||||||
import {orderedUnique} from "../utils/unique.js";
|
import {orderedUnique} from "../utils/unique.js";
|
||||||
|
|
||||||
export class ServerConsentViewModel extends ViewModel {
|
export class ServerConsentViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this.servers = options.servers;
|
this.servers = options.servers;
|
||||||
this.done = options.done;
|
this.done = options.done;
|
||||||
this.selectedServer = this.servers[0];
|
this.selectedServer = this.servers[0];
|
||||||
this.showSelectServer = false;
|
this.showSelectServer = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowServers() {
|
setShowServers() {
|
||||||
this.showSelectServer = true;
|
this.showSelectServer = true;
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Maturity, Platform, LinkKind,
|
import {Maturity, Platform, LinkKind,
|
||||||
FDroidLink, AppleStoreLink, PlayStoreLink, WebsiteLink} from "../types.js";
|
FDroidLink, AppleStoreLink, PlayStoreLink, WebsiteLink} from "../types.js";
|
||||||
|
|
||||||
const trustedWebInstances = [
|
const trustedWebInstances = [
|
||||||
"app.element.io", // first one is the default one
|
"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.
|
* Information on how to deep link to a given matrix client.
|
||||||
*/
|
*/
|
||||||
export class Element {
|
export class Element {
|
||||||
get id() { return "element.io"; }
|
get id() { return "element.io"; }
|
||||||
|
|
||||||
get platforms() {
|
get platforms() {
|
||||||
return [
|
return [
|
||||||
Platform.Android, Platform.iOS,
|
Platform.Android, Platform.iOS,
|
||||||
Platform.Windows, Platform.macOS, Platform.Linux,
|
Platform.Windows, Platform.macOS, Platform.Linux,
|
||||||
Platform.DesktopWeb
|
Platform.DesktopWeb
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
get icon() { return "images/client-icons/element.svg"; }
|
get icon() { return "images/client-icons/element.svg"; }
|
||||||
get appleAssociatedAppId() { return "7J4U792NQT.im.vector.app"; }
|
get appleAssociatedAppId() { return "7J4U792NQT.im.vector.app"; }
|
||||||
get name() {return "Element"; }
|
get name() {return "Element"; }
|
||||||
get description() { return 'Fully-featured Matrix client, used by millions.'; }
|
get description() { return 'Fully-featured Matrix client, used by millions.'; }
|
||||||
get homepage() { return "https://element.io"; }
|
get homepage() { return "https://element.io"; }
|
||||||
get author() { return "Element"; }
|
get author() { return "Element"; }
|
||||||
getMaturity(platform) { return Maturity.Stable; }
|
getMaturity(platform) { return Maturity.Stable; }
|
||||||
|
|
||||||
getDeepLink(platform, link) {
|
getDeepLink(platform, link) {
|
||||||
let fragmentPath;
|
let fragmentPath;
|
||||||
switch (link.kind) {
|
switch (link.kind) {
|
||||||
case LinkKind.User:
|
case LinkKind.User:
|
||||||
fragmentPath = `user/${link.identifier}`;
|
fragmentPath = `user/${link.identifier}`;
|
||||||
break;
|
break;
|
||||||
case LinkKind.Room:
|
case LinkKind.Room:
|
||||||
fragmentPath = `room/${link.identifier}`;
|
fragmentPath = `room/${link.identifier}`;
|
||||||
break;
|
break;
|
||||||
case LinkKind.Group:
|
case LinkKind.Group:
|
||||||
fragmentPath = `group/${link.identifier}`;
|
fragmentPath = `group/${link.identifier}`;
|
||||||
break;
|
break;
|
||||||
case LinkKind.Event:
|
case LinkKind.Event:
|
||||||
fragmentPath = `room/${link.identifier}/${link.eventId}`;
|
fragmentPath = `room/${link.identifier}/${link.eventId}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const isWebPlatform = platform === Platform.DesktopWeb || platform === Platform.MobileWeb;
|
const isWebPlatform = platform === Platform.DesktopWeb || platform === Platform.MobileWeb;
|
||||||
if (isWebPlatform || platform === Platform.iOS) {
|
if (isWebPlatform || platform === Platform.iOS) {
|
||||||
let instanceHost = trustedWebInstances[0];
|
let instanceHost = trustedWebInstances[0];
|
||||||
// we use app.element.io which iOS will intercept, but it likely won't intercept any other trusted instances
|
// 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.
|
// so only use a preferred web instance for true web links.
|
||||||
if (isWebPlatform && trustedWebInstances.includes(link.webInstances[this.id])) {
|
if (isWebPlatform && trustedWebInstances.includes(link.webInstances[this.id])) {
|
||||||
instanceHost = link.webInstances[this.id];
|
instanceHost = link.webInstances[this.id];
|
||||||
}
|
}
|
||||||
return `https://${instanceHost}/#/${fragmentPath}`;
|
return `https://${instanceHost}/#/${fragmentPath}`;
|
||||||
} else if (platform === Platform.Linux || platform === Platform.Windows || platform === Platform.macOS) {
|
} else if (platform === Platform.Linux || platform === Platform.Windows || platform === Platform.macOS) {
|
||||||
return `element://vector/webapp/#/${fragmentPath}`;
|
return `element://vector/webapp/#/${fragmentPath}`;
|
||||||
} else {
|
} else {
|
||||||
return `element://${fragmentPath}`;
|
return `element://${fragmentPath}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getLinkInstructions(platform, link) {}
|
getLinkInstructions(platform, link) {}
|
||||||
getCopyString(platform, link) {}
|
getCopyString(platform, link) {}
|
||||||
getInstallLinks(platform) {
|
getInstallLinks(platform) {
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case Platform.iOS: return [new AppleStoreLink('vector', 'id1083446067')];
|
case Platform.iOS: return [new AppleStoreLink('vector', 'id1083446067')];
|
||||||
case Platform.Android: return [new PlayStoreLink('im.vector.app'), new FDroidLink('im.vector.app')];
|
case Platform.Android: return [new PlayStoreLink('im.vector.app'), new FDroidLink('im.vector.app')];
|
||||||
default: return [new WebsiteLink("https://element.io/get-started")];
|
default: return [new WebsiteLink("https://element.io/get-started")];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canInterceptMatrixToLinks(platform) {
|
canInterceptMatrixToLinks(platform) {
|
||||||
return platform === Platform.Android;
|
return platform === Platform.Android;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreferredWebInstance(link) {
|
getPreferredWebInstance(link) {
|
||||||
const idx = trustedWebInstances.indexOf(link.webInstances[this.id])
|
const idx = trustedWebInstances.indexOf(link.webInstances[this.id])
|
||||||
|
|
|
@ -20,22 +20,22 @@ import {Maturity, Platform, LinkKind, FlathubLink} from "../types.js";
|
||||||
* Information on how to deep link to a given matrix client.
|
* Information on how to deep link to a given matrix client.
|
||||||
*/
|
*/
|
||||||
export class Fractal {
|
export class Fractal {
|
||||||
get id() { return "fractal"; }
|
get id() { return "fractal"; }
|
||||||
get name() { return "Fractal"; }
|
get name() { return "Fractal"; }
|
||||||
get icon() { return "images/client-icons/fractal.png"; }
|
get icon() { return "images/client-icons/fractal.png"; }
|
||||||
get author() { return "Daniel Garcia Moreno"; }
|
get author() { return "Daniel Garcia Moreno"; }
|
||||||
get homepage() { return "https://gitlab.gnome.org/GNOME/fractal"; }
|
get homepage() { return "https://gitlab.gnome.org/GNOME/fractal"; }
|
||||||
get platforms() { return [Platform.Linux]; }
|
get platforms() { return [Platform.Linux]; }
|
||||||
get description() { return 'Fractal is a Matrix Client written in Rust.'; }
|
get description() { return 'Fractal is a Matrix Client written in Rust.'; }
|
||||||
getMaturity(platform) { return Maturity.Beta; }
|
getMaturity(platform) { return Maturity.Beta; }
|
||||||
getDeepLink(platform, link) {}
|
getDeepLink(platform, link) {}
|
||||||
canInterceptMatrixToLinks(platform) { return false; }
|
canInterceptMatrixToLinks(platform) { return false; }
|
||||||
|
|
||||||
getLinkInstructions(platform, link) {
|
getLinkInstructions(platform, link) {
|
||||||
if (link.kind === LinkKind.User || link.kind === LinkKind.Room) {
|
if (link.kind === LinkKind.User || link.kind === LinkKind.Room) {
|
||||||
return "Click the '+' button in the top right and paste the identifier";
|
return "Click the '+' button in the top right and paste the identifier";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCopyString(platform, link) {
|
getCopyString(platform, link) {
|
||||||
if (link.kind === LinkKind.User || link.kind === LinkKind.Room) {
|
if (link.kind === LinkKind.User || link.kind === LinkKind.Room) {
|
||||||
|
@ -43,7 +43,7 @@ export class Fractal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getInstallLinks(platform) {
|
getInstallLinks(platform) {
|
||||||
if (platform === Platform.Linux) {
|
if (platform === Platform.Linux) {
|
||||||
return [new FlathubLink("org.gnome.Fractal")];
|
return [new FlathubLink("org.gnome.Fractal")];
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,49 +20,49 @@ import {Maturity, Platform, LinkKind, FlathubLink, style} from "../types.js";
|
||||||
* Information on how to deep link to a given matrix client.
|
* Information on how to deep link to a given matrix client.
|
||||||
*/
|
*/
|
||||||
export class Nheko {
|
export class Nheko {
|
||||||
get id() { return "nheko"; }
|
get id() { return "nheko"; }
|
||||||
get name() { return "Nheko"; }
|
get name() { return "Nheko"; }
|
||||||
get icon() { return "images/client-icons/nheko.svg"; }
|
get icon() { return "images/client-icons/nheko.svg"; }
|
||||||
get author() { return "mujx, red_sky, deepbluev7, Konstantinos Sideris"; }
|
get author() { return "mujx, red_sky, deepbluev7, Konstantinos Sideris"; }
|
||||||
get homepage() { return "https://github.com/Nheko-Reborn/nheko"; }
|
get homepage() { return "https://github.com/Nheko-Reborn/nheko"; }
|
||||||
get platforms() { return [Platform.Windows, Platform.macOS, Platform.Linux]; }
|
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.'; }
|
get description() { return 'A native desktop app for Matrix that feels more like a mainstream chat app.'; }
|
||||||
getMaturity(platform) { return Maturity.Beta; }
|
getMaturity(platform) { return Maturity.Beta; }
|
||||||
getDeepLink(platform, link) {
|
getDeepLink(platform, link) {
|
||||||
if (platform === Platform.Linux || platform === Platform.Windows) {
|
if (platform === Platform.Linux || platform === Platform.Windows) {
|
||||||
let identifier = encodeURIComponent(link.identifier.substring(1));
|
let identifier = encodeURIComponent(link.identifier.substring(1));
|
||||||
let isRoomid = link.identifier.substring(0, 1) === '!';
|
let isRoomid = link.identifier.substring(0, 1) === '!';
|
||||||
let fragmentPath;
|
let fragmentPath;
|
||||||
switch (link.kind) {
|
switch (link.kind) {
|
||||||
case LinkKind.User:
|
case LinkKind.User:
|
||||||
fragmentPath = `u/${identifier}?action=chat`;
|
fragmentPath = `u/${identifier}?action=chat`;
|
||||||
break;
|
break;
|
||||||
case LinkKind.Room:
|
case LinkKind.Room:
|
||||||
case LinkKind.Event:
|
case LinkKind.Event:
|
||||||
if (isRoomid)
|
if (isRoomid)
|
||||||
fragmentPath = `roomid/${identifier}`;
|
fragmentPath = `roomid/${identifier}`;
|
||||||
else
|
else
|
||||||
fragmentPath = `r/${identifier}`;
|
fragmentPath = `r/${identifier}`;
|
||||||
|
|
||||||
if (link.kind === LinkKind.Event)
|
if (link.kind === LinkKind.Event)
|
||||||
fragmentPath += `/e/${encodeURIComponent(link.eventId.substring(1))}`;
|
fragmentPath += `/e/${encodeURIComponent(link.eventId.substring(1))}`;
|
||||||
fragmentPath += '?action=join';
|
fragmentPath += '?action=join';
|
||||||
fragmentPath += link.servers.map(server => `&via=${encodeURIComponent(server)}`).join('');
|
fragmentPath += link.servers.map(server => `&via=${encodeURIComponent(server)}`).join('');
|
||||||
break;
|
break;
|
||||||
case LinkKind.Group:
|
case LinkKind.Group:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return `matrix:${fragmentPath}`;
|
return `matrix:${fragmentPath}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
canInterceptMatrixToLinks(platform) { return false; }
|
canInterceptMatrixToLinks(platform) { return false; }
|
||||||
|
|
||||||
getLinkInstructions(platform, link) {
|
getLinkInstructions(platform, link) {
|
||||||
switch (link.kind) {
|
switch (link.kind) {
|
||||||
case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)];
|
case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)];
|
||||||
case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)];
|
case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCopyString(platform, link) {
|
getCopyString(platform, link) {
|
||||||
switch (link.kind) {
|
switch (link.kind) {
|
||||||
|
@ -71,7 +71,7 @@ export class Nheko {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getInstallLinks(platform) {
|
getInstallLinks(platform) {
|
||||||
if (platform === Platform.Linux) {
|
if (platform === Platform.Linux) {
|
||||||
return [new FlathubLink("io.github.NhekoReborn.Nheko")];
|
return [new FlathubLink("io.github.NhekoReborn.Nheko")];
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,23 +20,23 @@ import {Maturity, Platform, LinkKind, WebsiteLink, style} from "../types.js";
|
||||||
* Information on how to deep link to a given matrix client.
|
* Information on how to deep link to a given matrix client.
|
||||||
*/
|
*/
|
||||||
export class Weechat {
|
export class Weechat {
|
||||||
get id() { return "weechat"; }
|
get id() { return "weechat"; }
|
||||||
get name() { return "Weechat"; }
|
get name() { return "Weechat"; }
|
||||||
get icon() { return "images/client-icons/weechat.svg"; }
|
get icon() { return "images/client-icons/weechat.svg"; }
|
||||||
get author() { return "Poljar"; }
|
get author() { return "Poljar"; }
|
||||||
get homepage() { return "https://github.com/poljar/weechat-matrix"; }
|
get homepage() { return "https://github.com/poljar/weechat-matrix"; }
|
||||||
get platforms() { return [Platform.Windows, Platform.macOS, Platform.Linux]; }
|
get platforms() { return [Platform.Windows, Platform.macOS, Platform.Linux]; }
|
||||||
get description() { return 'Command-line Matrix interface using Weechat.'; }
|
get description() { return 'Command-line Matrix interface using Weechat.'; }
|
||||||
getMaturity(platform) { return Maturity.Beta; }
|
getMaturity(platform) { return Maturity.Beta; }
|
||||||
getDeepLink(platform, link) {}
|
getDeepLink(platform, link) {}
|
||||||
canInterceptMatrixToLinks(platform) { return false; }
|
canInterceptMatrixToLinks(platform) { return false; }
|
||||||
|
|
||||||
getLinkInstructions(platform, link) {
|
getLinkInstructions(platform, link) {
|
||||||
switch (link.kind) {
|
switch (link.kind) {
|
||||||
case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)];
|
case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)];
|
||||||
case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)];
|
case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCopyString(platform, link) {
|
getCopyString(platform, link) {
|
||||||
switch (link.kind) {
|
switch (link.kind) {
|
||||||
|
@ -45,7 +45,7 @@ export class Weechat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getInstallLinks(platform) {}
|
getInstallLinks(platform) {}
|
||||||
|
|
||||||
getPreferredWebInstance(link) {}
|
getPreferredWebInstance(link) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,13 +23,13 @@ import {Tensor} from "./Tensor.js";
|
||||||
import {Fluffychat} from "./Fluffychat.js";
|
import {Fluffychat} from "./Fluffychat.js";
|
||||||
|
|
||||||
export function createClients() {
|
export function createClients() {
|
||||||
return [
|
return [
|
||||||
new Element(),
|
new Element(),
|
||||||
new Weechat(),
|
new Weechat(),
|
||||||
new Nheko(),
|
new Nheko(),
|
||||||
new Fractal(),
|
new Fractal(),
|
||||||
new Quaternion(),
|
new Quaternion(),
|
||||||
new Tensor(),
|
new Tensor(),
|
||||||
new Fluffychat(),
|
new Fluffychat(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,8 @@ export {Platform} from "../Platform.js";
|
||||||
|
|
||||||
export class AppleStoreLink {
|
export class AppleStoreLink {
|
||||||
constructor(org, appId) {
|
constructor(org, appId) {
|
||||||
this._org = org;
|
this._org = org;
|
||||||
this._appId = appId;
|
this._appId = appId;
|
||||||
}
|
}
|
||||||
|
|
||||||
createInstallURL(link) {
|
createInstallURL(link) {
|
||||||
|
@ -40,7 +40,7 @@ export class AppleStoreLink {
|
||||||
|
|
||||||
export class PlayStoreLink {
|
export class PlayStoreLink {
|
||||||
constructor(appId) {
|
constructor(appId) {
|
||||||
this._appId = appId;
|
this._appId = appId;
|
||||||
}
|
}
|
||||||
|
|
||||||
createInstallURL(link) {
|
createInstallURL(link) {
|
||||||
|
@ -58,7 +58,7 @@ export class PlayStoreLink {
|
||||||
|
|
||||||
export class FDroidLink {
|
export class FDroidLink {
|
||||||
constructor(appId) {
|
constructor(appId) {
|
||||||
this._appId = appId;
|
this._appId = appId;
|
||||||
}
|
}
|
||||||
|
|
||||||
createInstallURL(link) {
|
createInstallURL(link) {
|
||||||
|
@ -94,7 +94,7 @@ export class FlathubLink {
|
||||||
|
|
||||||
export class WebsiteLink {
|
export class WebsiteLink {
|
||||||
constructor(url) {
|
constructor(url) {
|
||||||
this._url = url;
|
this._url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
createInstallURL(link) {
|
createInstallURL(link) {
|
||||||
|
|
|
@ -17,10 +17,10 @@ limitations under the License.
|
||||||
import {TemplateView} from "../utils/TemplateView.js";
|
import {TemplateView} from "../utils/TemplateView.js";
|
||||||
|
|
||||||
export class LoadServerPolicyView extends TemplateView {
|
export class LoadServerPolicyView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.div({className: "LoadServerPolicyView card"}, [
|
return t.div({className: "LoadServerPolicyView card"}, [
|
||||||
t.div({className: {spinner: true, hidden: vm => !vm.loading}}),
|
t.div({className: {spinner: true, hidden: vm => !vm.loading}}),
|
||||||
t.h2(vm => vm.message)
|
t.h2(vm => vm.message)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,12 @@ import {ViewModel} from "../utils/ViewModel.js";
|
||||||
import {resolveServer} from "../preview/HomeServer.js";
|
import {resolveServer} from "../preview/HomeServer.js";
|
||||||
|
|
||||||
export class LoadServerPolicyViewModel extends ViewModel {
|
export class LoadServerPolicyViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this.server = options.server;
|
this.server = options.server;
|
||||||
this.message = `Looking up ${this.server} privacy policy…`;
|
this.message = `Looking up ${this.server} privacy policy…`;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
|
@ -20,61 +20,76 @@ function noTrailingSlash(url) {
|
||||||
|
|
||||||
export async function resolveServer(request, baseURL) {
|
export async function resolveServer(request, baseURL) {
|
||||||
baseURL = noTrailingSlash(baseURL);
|
baseURL = noTrailingSlash(baseURL);
|
||||||
if (!baseURL.startsWith("http://") && !baseURL.startsWith("https://")) {
|
if (!baseURL.startsWith("http://") && !baseURL.startsWith("https://")) {
|
||||||
baseURL = `https://${baseURL}`;
|
baseURL = `https://${baseURL}`;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const {status, body} = await request(`${baseURL}/.well-known/matrix/client`, {method: "GET"}).response();
|
try {
|
||||||
if (status === 200) {
|
const {status, body} = await request(`${baseURL}/.well-known/matrix/client`, {method: "GET"}).response();
|
||||||
const proposedBaseURL = body?.['m.homeserver']?.base_url;
|
if (status === 200) {
|
||||||
if (typeof proposedBaseURL === "string") {
|
const proposedBaseURL = body?.['m.homeserver']?.base_url;
|
||||||
baseURL = noTrailingSlash(proposedBaseURL);
|
if (typeof proposedBaseURL === "string") {
|
||||||
}
|
baseURL = noTrailingSlash(proposedBaseURL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
} catch (e) {
|
||||||
const {status} = await request(`${baseURL}/_matrix/client/versions`, {method: "GET"}).response();
|
console.warn("Failed to fetch ${baseURL}/.well-known/matrix/client", e);
|
||||||
if (status !== 200) {
|
}
|
||||||
throw new Error(`Invalid versions response from ${baseURL}`);
|
}
|
||||||
}
|
{
|
||||||
}
|
const {status} = await request(`${baseURL}/_matrix/client/versions`, {method: "GET"}).response();
|
||||||
return new HomeServer(request, baseURL);
|
if (status !== 200) {
|
||||||
|
throw new Error(`Invalid versions response from ${baseURL}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new HomeServer(request, baseURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HomeServer {
|
export class HomeServer {
|
||||||
constructor(request, baseURL) {
|
constructor(request, baseURL) {
|
||||||
this._request = request;
|
this._request = request;
|
||||||
this.baseURL = baseURL;
|
this.baseURL = baseURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserProfile(userId) {
|
async getUserProfile(userId) {
|
||||||
const {body} = await this._request(`${this.baseURL}/_matrix/client/r0/profile/${encodeURIComponent(userId)}`).response();
|
const {body} = await this._request(`${this.baseURL}/_matrix/client/r0/profile/${encodeURIComponent(userId)}`).response();
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findPublicRoomById(roomId) {
|
// MSC3266 implementation
|
||||||
const {body, status} = await this._request(`${this.baseURL}/_matrix/client/r0/directory/list/room/${encodeURIComponent(roomId)}`).response();
|
async getRoomSummary(roomIdOrAlias, viaServers) {
|
||||||
if (status !== 200 || body.visibility !== "public") {
|
let query;
|
||||||
return;
|
if (viaServers.length > 0) {
|
||||||
}
|
query = "?" + viaServers.map(server => `via=${encodeURIComponent(server)}`).join('&');
|
||||||
let nextBatch;
|
}
|
||||||
do {
|
const {body, status} = await this._request(`${this.baseURL}/_matrix/client/unstable/im.nheko.summary/rooms/${encodeURIComponent(roomIdOrAlias)}/summary${query}`).response();
|
||||||
const queryParams = encodeQueryParams({limit: 10000, since: nextBatch});
|
if (status !== 200) return;
|
||||||
const {body, status} = await this._request(`${this.baseURL}/_matrix/client/r0/publicRooms?${queryParams}`).response();
|
return body;
|
||||||
nextBatch = body.next_batch;
|
}
|
||||||
const publicRoom = body.chunk.find(c => c.room_id === roomId);
|
|
||||||
if (publicRoom) {
|
|
||||||
return publicRoom;
|
|
||||||
}
|
|
||||||
} while (nextBatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRoomIdFromAlias(alias) {
|
async findPublicRoomById(roomId) {
|
||||||
const {status, body} = await this._request(`${this.baseURL}/_matrix/client/r0/directory/room/${encodeURIComponent(alias)}`).response();
|
const {body, status} = await this._request(`${this.baseURL}/_matrix/client/r0/directory/list/room/${encodeURIComponent(roomId)}`).response();
|
||||||
if (status === 200) {
|
if (status !== 200 || body.visibility !== "public") {
|
||||||
return body.room_id;
|
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") {
|
async getPrivacyPolicyUrl(lang = "en") {
|
||||||
const headers = new Map();
|
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);
|
const parts = parseMxcUrl(url);
|
||||||
if (parts) {
|
if (parts) {
|
||||||
const [serverName, mediaId] = parts;
|
const [serverName, mediaId] = parts;
|
||||||
|
|
|
@ -43,7 +43,7 @@ class LoadingPreviewView extends TemplateView {
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoadedPreviewView extends TemplateView {
|
class LoadedPreviewView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
const avatar = t.map(vm => vm.avatarUrl, (avatarUrl, t) => {
|
const avatar = t.map(vm => vm.avatarUrl, (avatarUrl, t) => {
|
||||||
if (avatarUrl) {
|
if (avatarUrl) {
|
||||||
return t.img({className: "avatar", src: 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: "defaultAvatar"});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return t.div([
|
return t.div({className: vm.isSpaceRoom ? "mxSpace" : undefined}, [
|
||||||
t.div({className: "avatarContainer"}, avatar),
|
t.div({className: "avatarContainer"}, avatar),
|
||||||
t.h1(vm => vm.name),
|
t.h1(vm => vm.name),
|
||||||
t.p({className: {identifier: true, hidden: vm => !vm.identifier}}, vm => vm.identifier),
|
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.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]),
|
t.p({className: {topic: true, hidden: vm => !vm.topic}}, [vm => vm.topic]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,92 +21,101 @@ import {ClientListViewModel} from "../open/ClientListViewModel.js";
|
||||||
import {ClientViewModel} from "../open/ClientViewModel.js";
|
import {ClientViewModel} from "../open/ClientViewModel.js";
|
||||||
|
|
||||||
export class PreviewViewModel extends ViewModel {
|
export class PreviewViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
const { link, consentedServers } = options;
|
const { link, consentedServers } = options;
|
||||||
this._link = link;
|
this._link = link;
|
||||||
this._consentedServers = consentedServers;
|
this._consentedServers = consentedServers;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.name = this._link.identifier;
|
this.name = this._link.identifier;
|
||||||
this.avatarUrl = null;
|
this.avatarUrl = null;
|
||||||
this.identifier = null;
|
this.identifier = null;
|
||||||
this.memberCount = null;
|
this.memberCount = null;
|
||||||
this.topic = null;
|
this.topic = null;
|
||||||
this.domain = null;
|
this.domain = null;
|
||||||
this.failed = false;
|
this.failed = false;
|
||||||
}
|
this.isSpaceRoom = false;
|
||||||
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const {kind} = this._link;
|
const {kind} = this._link;
|
||||||
const supportsPreview = kind === LinkKind.User || kind === LinkKind.Room || kind === LinkKind.Event;
|
const supportsPreview = kind === LinkKind.User || kind === LinkKind.Room || kind === LinkKind.Event;
|
||||||
if (supportsPreview) {
|
if (supportsPreview) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
for (const server of this._consentedServers) {
|
for (const server of this._consentedServers) {
|
||||||
try {
|
try {
|
||||||
const homeserver = await resolveServer(this.request, server);
|
const homeserver = await resolveServer(this.request, server);
|
||||||
switch (this._link.kind) {
|
switch (this._link.kind) {
|
||||||
case LinkKind.User:
|
case LinkKind.User:
|
||||||
await this._loadUserPreview(homeserver, this._link.identifier);
|
await this._loadUserPreview(homeserver, this._link.identifier);
|
||||||
break;
|
break;
|
||||||
case LinkKind.Room:
|
case LinkKind.Room:
|
||||||
case LinkKind.Event:
|
case LinkKind.Event:
|
||||||
await this._loadRoomPreview(homeserver, this._link);
|
await this._loadRoomPreview(homeserver, this._link);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// assume we're done if nothing threw
|
// assume we're done if nothing threw
|
||||||
this.domain = server;
|
this.domain = server;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
return;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this._setNoPreview(this._link);
|
this._setNoPreview(this._link);
|
||||||
if (this._consentedServers.length && supportsPreview) {
|
if (this._consentedServers.length && supportsPreview) {
|
||||||
this.domain = this._consentedServers[this._consentedServers.length - 1];
|
this.domain = this._consentedServers[this._consentedServers.length - 1];
|
||||||
this.failed = true;
|
this.failed = true;
|
||||||
}
|
}
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasTopic() { return this._link.kind === LinkKind.Room; }
|
get hasTopic() { return this._link.kind === LinkKind.Room; }
|
||||||
get hasMemberCount() { return this.hasTopic; }
|
get hasMemberCount() { return this.hasTopic; }
|
||||||
|
|
||||||
async _loadUserPreview(homeserver, userId) {
|
async _loadUserPreview(homeserver, userId) {
|
||||||
const profile = await homeserver.getUserProfile(userId);
|
const profile = await homeserver.getUserProfile(userId);
|
||||||
this.name = profile.displayname || userId;
|
this.name = profile.displayname || userId;
|
||||||
this.avatarUrl = profile.avatar_url ?
|
this.avatarUrl = profile.avatar_url ?
|
||||||
homeserver.mxcUrlThumbnail(profile.avatar_url, 64, 64, "crop") :
|
homeserver.mxcUrlThumbnail(profile.avatar_url, 64, 64, "crop") :
|
||||||
null;
|
null;
|
||||||
this.identifier = userId;
|
this.identifier = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadRoomPreview(homeserver, link) {
|
async _loadRoomPreview(homeserver, link) {
|
||||||
let publicRoom;
|
let publicRoom;
|
||||||
if (link.identifierKind === IdentifierKind.RoomId) {
|
if (link.identifierKind === IdentifierKind.RoomId || link.identifierKind === IdentifierKind.RoomAlias) {
|
||||||
publicRoom = await homeserver.findPublicRoomById(link.identifier);
|
publicRoom = await homeserver.getRoomSummary(link.identifier, link.servers);
|
||||||
} else if (link.identifierKind === IdentifierKind.RoomAlias) {
|
}
|
||||||
const roomId = await homeserver.getRoomIdFromAlias(link.identifier);
|
|
||||||
if (roomId) {
|
if (!publicRoom) {
|
||||||
publicRoom = await homeserver.findPublicRoomById(roomId);
|
if (link.identifierKind === IdentifierKind.RoomId) {
|
||||||
}
|
publicRoom = await homeserver.findPublicRoomById(link.identifier);
|
||||||
}
|
} else if (link.identifierKind === IdentifierKind.RoomAlias) {
|
||||||
this.name = publicRoom?.name || publicRoom?.canonical_alias || link.identifier;
|
const roomId = await homeserver.getRoomIdFromAlias(link.identifier);
|
||||||
this.avatarUrl = publicRoom?.avatar_url ?
|
if (roomId) {
|
||||||
homeserver.mxcUrlThumbnail(publicRoom.avatar_url, 64, 64, "crop") :
|
publicRoom = await homeserver.findPublicRoomById(roomId);
|
||||||
null;
|
}
|
||||||
this.memberCount = publicRoom?.num_joined_members;
|
}
|
||||||
this.topic = publicRoom?.topic;
|
}
|
||||||
this.identifier = publicRoom?.canonical_alias || link.identifier;
|
|
||||||
|
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) {
|
if (this.identifier === this.name) {
|
||||||
this.identifier = null;
|
this.identifier = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_setNoPreview(link) {
|
_setNoPreview(link) {
|
||||||
this.name = link.identifier;
|
this.name = link.identifier;
|
||||||
|
|
Loading…
Reference in a new issue