diff --git a/.gitignore b/.gitignore index df36431e07d51f319ab23da39ad2263f60b0d088..e80463637630a65c636585e7dfba50cf54982be2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules build *.tar.gz +/.idea diff --git a/css/main.css b/css/main.css index f9532509d4be7ed0a7bba6d9b4bdd097658428af..283adef110cf32228a4a6158871e429d5f891b7a 100644 --- a/css/main.css +++ b/css/main.css @@ -21,31 +21,31 @@ limitations under the License. @import url('open.css'); :root { - --app-background: #f4f4f4; - --background: #ffffff; - --foreground: #000000; - --font: #333333; - --grey: #666666; - --accent: #0098d4; - --error: #d6001c; - --link: #0098d4; - --borders: #f4f4f4; + --app-background: #f4f4f4; + --background: #ffffff; + --foreground: #000000; + --font: #333333; + --grey: #666666; + --accent: #0098d4; + --error: #d6001c; + --link: #0098d4; + --borders: #f4f4f4; --lightgrey: #E6E6E6; --spinner-stroke-size: 2px; } html { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } body { - background-color: var(--app-background); - background-image: url('../images/background.svg'); + background-color: var(--app-background); + background-image: url('../images/background.svg'); background-attachment: fixed; - background-repeat: no-repeat; - background-size: auto; - background-position: center -50px; + background-repeat: no-repeat; + background-size: auto; + background-position: center -50px; height: 100%; width: 100%; font-size: 14px; @@ -89,12 +89,12 @@ input[type="checkbox"], input[type="radio"] { .RootView { margin: 0 auto; - max-width: 480px; - width: 100%; + max-width: 480px; + width: 100%; } .card { - background-color: var(--background); + background-color: var(--background); border-radius: 16px; box-shadow: 0px 18px 24px rgba(0, 0, 0, 0.06); } @@ -104,20 +104,20 @@ input[type="checkbox"], input[type="radio"] { } .hidden { - display: none !important; + display: none !important; } @media screen and (max-width: 480px) { body { - background-image: none; - background-color: var(--background); - padding: 0; + background-image: none; + background-color: var(--background); + padding: 0; } .card { - border-radius: unset; - box-shadow: unset; + border-radius: unset; + box-shadow: unset; } } @@ -141,7 +141,7 @@ input[type="checkbox"], input[type="radio"] { } a, button.text { - color: var(--link); + color: var(--link); } button.text { diff --git a/css/preview.css b/css/preview.css index 05b569b40d118a31228e7bfd3a822a462bbe5ddd..aed71116ff4fd5f619754886ed3962708dbfa999 100644 --- a/css/preview.css +++ b/css/preview.css @@ -22,6 +22,10 @@ height: 64px; } +.PreviewView .mxSpace .avatar { + border-radius: 12px; +} + .PreviewView .defaultAvatar { width: 64px; height: 64px; diff --git a/images/client-icons/fluffychat.svg b/images/client-icons/fluffychat.svg index a3a95164516eb7a5f52d8a3ce70cacc4ee0debd1..934ebd90e0f7cdfbe2f5ddb40057263f7035c99f 100644 --- a/images/client-icons/fluffychat.svg +++ b/images/client-icons/fluffychat.svg @@ -1,43 +1,43 @@ <?xml version="1.0" encoding="utf-8"?> <!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 181.4 181.9" style="enable-background:new 0 0 181.4 181.9;" xml:space="preserve"> + viewBox="0 0 181.4 181.9" style="enable-background:new 0 0 181.4 181.9;" xml:space="preserve"> <style type="text/css"> - .st0{fill:url(#SVGID_1_);} - .st1{fill:#F094BE;} - .st2{fill:#4D3F92;} - .st3{fill:#FFFFFF;} + .st0{fill:url(#SVGID_1_);} + .st1{fill:#F094BE;} + .st2{fill:#4D3F92;} + .st3{fill:#FFFFFF;} </style> <g id="Capa_1"> - <rect x="0" y="0" style="color:#FFFFFF" width="181.4" height="181.9" class="st3"/> + <rect x="0" y="0" style="color:#FFFFFF" width="181.4" height="181.9" class="st3"/> </g> <g id="Capa_2"> - <g> - <path class="st2" d="M151.6,95.1c1.5-0.3,2.8-1,3.8-2c4-5.3,0.8-11.8-4.5-12.6c-0.8,0-1.5-0.8-1.5-1.5c0-0.3,0-0.5,0-0.5 - c0.8-0.8,1.5-1.8,2.5-3.3c8.1-10.8,11.8-50.6,3.8-53.7c-9.8-3.3-29.7,6.3-38.3,17.4c-0.5-0.3-1-1-1-1.8c0.3-3-1.3-5.5-3.5-6.8 - c-4.5-2.3-8.8,0-10.6,3.3c-0.5,0.8-1.3,1.3-2,1c-0.8,0-1.5-0.8-1.5-1.5c-0.5-2.5-2-4.5-4.3-5.5c-4.8-2-9.8,0.8-10.6,5.3 - c-0.3,0.8-0.8,1.5-1.5,1.5c-0.8,0.3-1.5-0.3-2-1c-1.5-2.3-4-3.8-6.5-3.8c-4,0-7.6,3.3-7.8,7.3v0.3v0.3c0,0.8-0.5,1.5-1,1.8h-0.3 - c-8.3-10.8-28.5-20.7-38.5-17.4c-8.1,2.8-4.3,42.6,4,53.4c1.5,2,2.8,3.5,3.8,4.5c-0.3,0.8-1,1.5-1.8,1.5c-1.3,0-2.5,0.5-3.5,1.3 - c-5.3,5-2.3,12.1,3,13.4c0.8,0.3,1.5,1,1.5,1.8c0,0.8-0.5,1.8-1.3,2c-1,0.5-2,1-2.8,2c-4,5.8,0,12.3,5.5,12.3 - c0.8,0,1.5,0.5,1.8,1.3c0.3,0.8,0.3,1.5-0.5,2c-1.5,1.5-2.3,3.5-2,5.5c0.3,2.8,2,5.3,4.8,6.5c1.5,0.8,3,0.8,4.5,0.5 - c0.8-0.3,1.5,0,2,0.8c0.5,0.5,0.5,1.5,0.3,2c-0.8,1.5-1,3.3-0.5,5c0.8,2.8,2.8,4.8,5.5,5.5c2.5,0.5,4.3-0.3,5.5-0.8 - c0.5-0.3-3.3,9.1-6,15.4c-0.8,2,1.3,4.3,3.5,3.3c8.3-3.8,22.2-10.3,22.2-9.8c0.5,5.3,6.5,9.1,12.3,5.3c1.3-0.8,2-2.3,2.3-3.5 - c0.3-0.8,1-1.5,2-1.5c1,0,1.8,0.5,2,1.5c0.3,1.3,0.8,2.3,1.8,3c5.8,4.5,12.3,0.8,12.8-4.8c0-0.8,0.5-1.5,1.3-1.8 - c0.8-0.3,1.5,0,2,0.5c1.5,1.5,3.3,2.5,5.3,2.5l0,0c2.5,0,5-1.3,6.5-3.8c1-1.5,1.3-3,1-5c0-0.8,0.3-1.5,0.8-2c0.5-0.5,1.5-0.5,2,0 - c1.5,0.8,3.3,1.3,5,0.8c2.8-0.5,5-2.8,5.8-5.3c0.5-1.8,0.3-3.5-0.5-5.3c-0.3-0.8-0.3-1.5,0.3-2s1.3-0.8,2-0.8 - c1.8,0.3,3.3,0.3,4.8-0.5c2.3-1,3.8-3,4.3-5.5c0.5-2.5-0.3-4.8-2-6.5c-0.5-0.5-0.8-1.3-0.5-2s1-1.3,1.8-1.3c1.8,0,3.8-0.5,5-2 - c4.3-4.5,2.3-10.6-2.5-12.6c-0.8-0.3-1.3-1-1.3-2C150.1,95.8,150.8,95.1,151.6,95.1z"/> - <path class="st3" d="M131.4,42.2c0.5,1.5,0.5,3,0,4.5c-0.3,0.8,0,1.5,0.5,2s1.3,0.8,2,0.5c1-0.5,2-0.5,3-0.5c2.3,0,4.3,1,5.8,3 - c1,1.3,1.8,3,1.5,4.8c0,1.5-0.5,2.8-1.3,4c-0.5,0.5-0.5,1.5,0,2c0.3,0.3,0.5,0.8,1,0.8c1-0.3,2-1,2.8-2c4.5-6.3,5.3-26.2,0.8-27.7 - c-4.5-1.5-12.3,1.5-17.9,6C130.7,40.1,131.2,40.9,131.4,42.2z"/> - <path class="st3" d="M39,63.6c0.3-0.3,0.5-0.5,0.8-0.8c0.5-0.8,0.3-1.5,0-2C38.5,59,38.2,57,38.5,55c0.5-2.8,2.8-5,5.5-5.8 - c1.5-0.5,3-0.3,4.5,0.3c0.8,0.3,1.5,0,2-0.5c0.5-0.5,0.8-1.3,0.5-2c-0.5-1.5-0.5-3,0-4.5c0.3-1,0.8-2,1.5-2.8 - c-5.5-4.5-13.9-7.8-18.4-6.3S30.4,54.8,35,61.1C36,62.6,37.2,63.3,39,63.6z"/> - <g> - <circle class="st3" cx="60.9" cy="94.6" r="9.3"/> - <path class="st3" d="M100.7,94.6c0,5.3-4.3,9.3-9.3,9.3c-5.3,0-9.3-4.3-9.3-9.3S100.7,89.3,100.7,94.6z"/> - <circle class="st3" cx="121.6" cy="94.6" r="9.3"/> - </g> - </g> + <g> + <path class="st2" d="M151.6,95.1c1.5-0.3,2.8-1,3.8-2c4-5.3,0.8-11.8-4.5-12.6c-0.8,0-1.5-0.8-1.5-1.5c0-0.3,0-0.5,0-0.5 + c0.8-0.8,1.5-1.8,2.5-3.3c8.1-10.8,11.8-50.6,3.8-53.7c-9.8-3.3-29.7,6.3-38.3,17.4c-0.5-0.3-1-1-1-1.8c0.3-3-1.3-5.5-3.5-6.8 + c-4.5-2.3-8.8,0-10.6,3.3c-0.5,0.8-1.3,1.3-2,1c-0.8,0-1.5-0.8-1.5-1.5c-0.5-2.5-2-4.5-4.3-5.5c-4.8-2-9.8,0.8-10.6,5.3 + c-0.3,0.8-0.8,1.5-1.5,1.5c-0.8,0.3-1.5-0.3-2-1c-1.5-2.3-4-3.8-6.5-3.8c-4,0-7.6,3.3-7.8,7.3v0.3v0.3c0,0.8-0.5,1.5-1,1.8h-0.3 + c-8.3-10.8-28.5-20.7-38.5-17.4c-8.1,2.8-4.3,42.6,4,53.4c1.5,2,2.8,3.5,3.8,4.5c-0.3,0.8-1,1.5-1.8,1.5c-1.3,0-2.5,0.5-3.5,1.3 + c-5.3,5-2.3,12.1,3,13.4c0.8,0.3,1.5,1,1.5,1.8c0,0.8-0.5,1.8-1.3,2c-1,0.5-2,1-2.8,2c-4,5.8,0,12.3,5.5,12.3 + c0.8,0,1.5,0.5,1.8,1.3c0.3,0.8,0.3,1.5-0.5,2c-1.5,1.5-2.3,3.5-2,5.5c0.3,2.8,2,5.3,4.8,6.5c1.5,0.8,3,0.8,4.5,0.5 + c0.8-0.3,1.5,0,2,0.8c0.5,0.5,0.5,1.5,0.3,2c-0.8,1.5-1,3.3-0.5,5c0.8,2.8,2.8,4.8,5.5,5.5c2.5,0.5,4.3-0.3,5.5-0.8 + c0.5-0.3-3.3,9.1-6,15.4c-0.8,2,1.3,4.3,3.5,3.3c8.3-3.8,22.2-10.3,22.2-9.8c0.5,5.3,6.5,9.1,12.3,5.3c1.3-0.8,2-2.3,2.3-3.5 + c0.3-0.8,1-1.5,2-1.5c1,0,1.8,0.5,2,1.5c0.3,1.3,0.8,2.3,1.8,3c5.8,4.5,12.3,0.8,12.8-4.8c0-0.8,0.5-1.5,1.3-1.8 + c0.8-0.3,1.5,0,2,0.5c1.5,1.5,3.3,2.5,5.3,2.5l0,0c2.5,0,5-1.3,6.5-3.8c1-1.5,1.3-3,1-5c0-0.8,0.3-1.5,0.8-2c0.5-0.5,1.5-0.5,2,0 + c1.5,0.8,3.3,1.3,5,0.8c2.8-0.5,5-2.8,5.8-5.3c0.5-1.8,0.3-3.5-0.5-5.3c-0.3-0.8-0.3-1.5,0.3-2s1.3-0.8,2-0.8 + c1.8,0.3,3.3,0.3,4.8-0.5c2.3-1,3.8-3,4.3-5.5c0.5-2.5-0.3-4.8-2-6.5c-0.5-0.5-0.8-1.3-0.5-2s1-1.3,1.8-1.3c1.8,0,3.8-0.5,5-2 + c4.3-4.5,2.3-10.6-2.5-12.6c-0.8-0.3-1.3-1-1.3-2C150.1,95.8,150.8,95.1,151.6,95.1z"/> + <path class="st3" d="M131.4,42.2c0.5,1.5,0.5,3,0,4.5c-0.3,0.8,0,1.5,0.5,2s1.3,0.8,2,0.5c1-0.5,2-0.5,3-0.5c2.3,0,4.3,1,5.8,3 + c1,1.3,1.8,3,1.5,4.8c0,1.5-0.5,2.8-1.3,4c-0.5,0.5-0.5,1.5,0,2c0.3,0.3,0.5,0.8,1,0.8c1-0.3,2-1,2.8-2c4.5-6.3,5.3-26.2,0.8-27.7 + c-4.5-1.5-12.3,1.5-17.9,6C130.7,40.1,131.2,40.9,131.4,42.2z"/> + <path class="st3" d="M39,63.6c0.3-0.3,0.5-0.5,0.8-0.8c0.5-0.8,0.3-1.5,0-2C38.5,59,38.2,57,38.5,55c0.5-2.8,2.8-5,5.5-5.8 + c1.5-0.5,3-0.3,4.5,0.3c0.8,0.3,1.5,0,2-0.5c0.5-0.5,0.8-1.3,0.5-2c-0.5-1.5-0.5-3,0-4.5c0.3-1,0.8-2,1.5-2.8 + c-5.5-4.5-13.9-7.8-18.4-6.3S30.4,54.8,35,61.1C36,62.6,37.2,63.3,39,63.6z"/> + <g> + <circle class="st3" cx="60.9" cy="94.6" r="9.3"/> + <path class="st3" d="M100.7,94.6c0,5.3-4.3,9.3-9.3,9.3c-5.3,0-9.3-4.3-9.3-9.3S100.7,89.3,100.7,94.6z"/> + <circle class="st3" cx="121.6" cy="94.6" r="9.3"/> + </g> + </g> </g> </svg> diff --git a/index.html b/index.html index 7f5fdc89c219220db9ef0db0dcd71f4cdbbcb0a6..dda600ccbf7366f0d3623254e82e183b0370864b 100644 --- a/index.html +++ b/index.html @@ -1,17 +1,17 @@ <!DOCTYPE html> <html> <head> - <meta charset="utf-8"> - <title>You're invited to talk on Matrix</title> - <meta name="description" content="You're invited to talk on Matrix"> - <meta name="viewport" content="width=device-width, user-scalable=no"> - <link rel="stylesheet" type="text/css" href="css/main.css"> + <meta charset="utf-8"> + <title>You're invited to talk on Matrix</title> + <meta name="description" content="You're invited to talk on Matrix"> + <meta name="viewport" content="width=device-width, user-scalable=no"> + <link rel="stylesheet" type="text/css" href="css/main.css"> </head> <body> - <script id="main" type="module"> - import {main} from "./src/main.js"; - main(document.body); - </script> + <script id="main" type="module"> + import {main} from "./src/main.js"; + main(document.body); + </script> <noscript> <h1>Please enable javascript</h1> <p>Matrix.to is a preview service from chat rooms, people and communities on <a href="https://matrix.org">Matrix</a>.</p> diff --git a/scripts/serve-local.js b/scripts/serve-local.js index 08e500d4abcd73f071c34713b5652514efa7ac24..b1b77e83d32ec28223c6072eb9a1661071ae43dd 100644 --- a/scripts/serve-local.js +++ b/scripts/serve-local.js @@ -23,25 +23,26 @@ const projectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ". // Serve up parent directory with cache disabled const serve = serveStatic( - projectDir, - { - etag: false, - setHeaders: res => { - res.setHeader("Pragma", "no-cache"); - res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - res.setHeader("Expires", "Wed, 21 Oct 2015 07:28:00 GMT"); + projectDir, + { + etag: false, + setHeaders: res => { + res.setHeader("Pragma", "no-cache"); + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + res.setHeader("Expires", "Wed, 21 Oct 2015 07:28:00 GMT"); // same CSP as matrix.to server is using, so local testing happens under similar environment res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src * data:; connect-src *; font-src 'self'; manifest-src 'self'; form-action 'self'; navigate-to *;"); - }, - index: ['index.html', 'index.htm'] - } + }, + index: ['index.html', 'index.htm'] + } ); // Create server const server = http.createServer(function onRequest (req, res) { console.log(req.method, req.url); - serve(req, res, finalhandler(req, res)) + serve(req, res, finalhandler(req, res)) }); // Listen server.listen(5000); +console.log("Listening on port 5000"); diff --git a/src/Link.js b/src/Link.js index f468bfb52747f11bf90d162e3c33ae27af8bf0db..079d7be4d7e7fc7455fbc0f24708f125ba9168f3 100644 --- a/src/Link.js +++ b/src/Link.js @@ -24,20 +24,20 @@ const EVENTID_PATTERN = /^$([^:]+):(.+)$/; const GROUPID_PATTERN = /^\+([^:]+):(.+)$/; export const IdentifierKind = createEnum( - "RoomId", - "RoomAlias", - "UserId", - "GroupId", + "RoomId", + "RoomAlias", + "UserId", + "GroupId", ); function asPrefix(identifierKind) { - switch (identifierKind) { - case IdentifierKind.RoomId: return "!"; - case IdentifierKind.RoomAlias: return "#"; - case IdentifierKind.GroupId: return "+"; - case IdentifierKind.UserId: return "@"; - default: throw new Error("invalid id kind " + identifierKind); - } + switch (identifierKind) { + case IdentifierKind.RoomId: return "!"; + case IdentifierKind.RoomAlias: return "#"; + case IdentifierKind.GroupId: return "+"; + case IdentifierKind.UserId: return "@"; + default: throw new Error("invalid id kind " + identifierKind); + } } function getWebInstanceMap(queryParams) { @@ -56,19 +56,19 @@ function getWebInstanceMap(queryParams) { } export function getLabelForLinkKind(kind) { - switch (kind) { - case LinkKind.User: return "Start chat"; - case LinkKind.Room: return "View room"; - case LinkKind.Group: return "View community"; - case LinkKind.Event: return "View message"; - } + switch (kind) { + case LinkKind.User: return "Start chat"; + case LinkKind.Room: return "View room"; + case LinkKind.Group: return "View community"; + case LinkKind.Event: return "View message"; + } } export const LinkKind = createEnum( - "Room", - "User", - "Group", - "Event" + "Room", + "User", + "Group", + "Event" ) export class Link { @@ -81,106 +81,106 @@ export class Link { ); } - static parse(fragment) { - if (!fragment) { - return null; - } - let [linkStr, queryParamsStr] = fragment.split("?"); + static parse(fragment) { + if (!fragment) { + return null; + } + let [linkStr, queryParamsStr] = fragment.split("?"); - let viaServers = []; + let viaServers = []; let clientId = null; let webInstances = {}; - if (queryParamsStr) { + if (queryParamsStr) { const queryParams = queryParamsStr.split("&").map(pair => { const [key, value] = pair.split("="); return [decodeURIComponent(key), decodeURIComponent(value)]; }); - viaServers = queryParams - .filter(([key, value]) => key === "via") - .map(([,value]) => value); + viaServers = queryParams + .filter(([key, value]) => key === "via") + .map(([,value]) => value); const clientParam = queryParams.find(([key]) => key === "client"); if (clientParam) { clientId = clientParam[1]; } webInstances = getWebInstanceMap(queryParams); - } + } - if (linkStr.startsWith("#/")) { - linkStr = linkStr.substr(2); - } + if (linkStr.startsWith("#/")) { + linkStr = linkStr.substr(2); + } const [identifier, eventId] = linkStr.split("/"); - let matches; - matches = USERID_PATTERN.exec(identifier); - if (matches) { - const server = matches[2]; - const localPart = matches[1]; - return new Link(clientId, viaServers, IdentifierKind.UserId, localPart, server, webInstances); - } - matches = ROOMALIAS_PATTERN.exec(identifier); - if (matches) { - const server = matches[2]; - const localPart = matches[1]; - return new Link(clientId, viaServers, IdentifierKind.RoomAlias, localPart, server, webInstances, eventId); - } - matches = ROOMID_PATTERN.exec(identifier); - if (matches) { - const server = matches[2]; - const localPart = matches[1]; - return new Link(clientId, viaServers, IdentifierKind.RoomId, localPart, server, webInstances, eventId); - } - matches = GROUPID_PATTERN.exec(identifier); - if (matches) { - const server = matches[2]; - const localPart = matches[1]; - return new Link(clientId, viaServers, IdentifierKind.GroupId, localPart, server, webInstances); - } - return null; - } - - constructor(clientId, viaServers, identifierKind, localPart, server, webInstances, eventId) { - const servers = [server]; - servers.push(...viaServers); + let matches; + matches = USERID_PATTERN.exec(identifier); + if (matches) { + const server = matches[2]; + const localPart = matches[1]; + return new Link(clientId, viaServers, IdentifierKind.UserId, localPart, server, webInstances); + } + matches = ROOMALIAS_PATTERN.exec(identifier); + if (matches) { + const server = matches[2]; + const localPart = matches[1]; + return new Link(clientId, viaServers, IdentifierKind.RoomAlias, localPart, server, webInstances, eventId); + } + matches = ROOMID_PATTERN.exec(identifier); + if (matches) { + const server = matches[2]; + const localPart = matches[1]; + return new Link(clientId, viaServers, IdentifierKind.RoomId, localPart, server, webInstances, eventId); + } + matches = GROUPID_PATTERN.exec(identifier); + if (matches) { + const server = matches[2]; + const localPart = matches[1]; + return new Link(clientId, viaServers, IdentifierKind.GroupId, localPart, server, webInstances); + } + return null; + } + + constructor(clientId, viaServers, identifierKind, localPart, server, webInstances, eventId) { + const servers = [server]; + servers.push(...viaServers); this.webInstances = webInstances; - this.servers = orderedUnique(servers); - this.identifierKind = identifierKind; - this.identifier = `${asPrefix(identifierKind)}${localPart}:${server}`; - this.eventId = eventId; + this.servers = orderedUnique(servers); + this.identifierKind = identifierKind; + this.identifier = `${asPrefix(identifierKind)}${localPart}:${server}`; + this.eventId = eventId; this.clientId = clientId; - } - - get kind() { - if (this.eventId) { - return LinkKind.Event; - } - switch (this.identifierKind) { - case IdentifierKind.RoomId: - case IdentifierKind.RoomAlias: - return LinkKind.Room; - case IdentifierKind.UserId: - return LinkKind.User; - case IdentifierKind.GroupId: - return LinkKind.Group; - default: - return null; - } - } - - equals(link) { - return link && - link.identifier === this.identifier && - this.servers.length === link.servers.length && - this.servers.every((s, i) => link.servers[i] === s) && + } + + get kind() { + if (this.eventId) { + return LinkKind.Event; + } + switch (this.identifierKind) { + case IdentifierKind.RoomId: + case IdentifierKind.RoomAlias: + return LinkKind.Room; + case IdentifierKind.UserId: + return LinkKind.User; + case IdentifierKind.GroupId: + return LinkKind.Group; + default: + return null; + } + } + + equals(link) { + return link && + link.identifier === this.identifier && + this.servers.length === link.servers.length && + this.servers.every((s, i) => link.servers[i] === s) && Object.keys(this.webInstances).length === Object.keys(link.webInstances).length && Object.keys(this.webInstances).every(k => this.webInstances[k] === link.webInstances[k]); - } - - toFragment() { - if (this.eventId) { - return `/${this.identifier}/${this.eventId}`; - } else { - return `/${this.identifier}`; - } - } + } + + toFragment() { + if (this.eventId) { + return `/${this.identifier}/${this.eventId}`; + } else { + return `/${this.identifier}`; + } + } } diff --git a/src/Platform.js b/src/Platform.js index 21de26485056451efdadbc50105c7f1e8ed66b9f..31562f488721fa2ab2158caf855f6dfb73309ebc 100644 --- a/src/Platform.js +++ b/src/Platform.js @@ -17,17 +17,17 @@ limitations under the License. import {createEnum} from "./utils/enum.js"; export const Platform = createEnum( - "DesktopWeb", - "MobileWeb", - "Android", - "iOS", - "Windows", - "macOS", - "Linux" + "DesktopWeb", + "MobileWeb", + "Android", + "iOS", + "Windows", + "macOS", + "Linux" ); export function guessApplicablePlatforms(userAgent, platform) { - // return [Platform.DesktopWeb, Platform.Linux]; + // return [Platform.DesktopWeb, Platform.Linux]; let nativePlatform; let webPlatform; if (/android/i.test(userAgent)) { @@ -55,10 +55,10 @@ export function guessApplicablePlatforms(userAgent, platform) { } export function isWebPlatform(p) { - return p === Platform.DesktopWeb || p === Platform.MobileWeb; + return p === Platform.DesktopWeb || p === Platform.MobileWeb; } export function isDesktopPlatform(p) { - return p === Platform.Linux || p === Platform.Windows || p === Platform.macOS; + return p === Platform.Linux || p === Platform.Windows || p === Platform.macOS; } diff --git a/src/Preferences.js b/src/Preferences.js index 53f55110c4f816943a3bf358adc6fe99211d5c93..d6d36f69a1b93e5d5418fd65cc52b3eb33b00afe 100644 --- a/src/Preferences.js +++ b/src/Preferences.js @@ -18,51 +18,51 @@ import {Platform} from "./Platform.js"; import {EventEmitter} from "./utils/ViewModel.js"; export class Preferences extends EventEmitter { - constructor(localStorage) { + constructor(localStorage) { super(); - this._localStorage = localStorage; - this.clientId = null; - // used to differentiate web from native if a client supports both - this.platform = null; - this.homeservers = null; + this._localStorage = localStorage; + this.clientId = null; + // used to differentiate web from native if a client supports both + this.platform = null; + this.homeservers = null; - const prefsStr = localStorage.getItem("preferred_client"); - if (prefsStr) { - const {id, platform} = JSON.parse(prefsStr); - this.clientId = id; - this.platform = Platform[platform]; - } + const prefsStr = localStorage.getItem("preferred_client"); + if (prefsStr) { + const {id, platform} = JSON.parse(prefsStr); + this.clientId = id; + this.platform = Platform[platform]; + } const serversStr = localStorage.getItem("consented_servers"); if (serversStr) { this.homeservers = JSON.parse(serversStr); } - } + } - setClient(id, platform) { - this.clientId = id; - platform = Platform[platform]; - this.platform = platform; - this._localStorage.setItem("preferred_client", JSON.stringify({id, platform})); + setClient(id, platform) { + this.clientId = id; + platform = Platform[platform]; + this.platform = platform; + this._localStorage.setItem("preferred_client", JSON.stringify({id, platform})); this.emit("canClear") - } + } - setHomeservers(homeservers, persist) { + setHomeservers(homeservers, persist) { this.homeservers = homeservers; if (persist) { this._localStorage.setItem("consented_servers", JSON.stringify(homeservers)); this.emit("canClear"); } - } + } - clear() { - this._localStorage.removeItem("preferred_client"); + clear() { + this._localStorage.removeItem("preferred_client"); this._localStorage.removeItem("consented_servers"); - this.clientId = null; - this.platform = null; + this.clientId = null; + this.platform = null; this.homeservers = null; - } + } - get canClear() { - return !!this.clientId || !!this.platform || !!this.homeservers; - } + get canClear() { + return !!this.clientId || !!this.platform || !!this.homeservers; + } } diff --git a/src/RootView.js b/src/RootView.js index eb8e66c62cb03324f83da834a2bdcfa0b79b9023..315fbd9663314fbcf02787e3559585d12d7262c0 100644 --- a/src/RootView.js +++ b/src/RootView.js @@ -20,25 +20,25 @@ import {CreateLinkView} from "./create/CreateLinkView.js"; import {LoadServerPolicyView} from "./policy/LoadServerPolicyView.js"; export class RootView extends TemplateView { - render(t, vm) { - return t.div({className: "RootView"}, [ - t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null), - t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null), + render(t, vm) { + return t.div({className: "RootView"}, [ + t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null), + t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null), t.mapView(vm => vm.loadServerPolicyViewModel, vm => vm ? new LoadServerPolicyView(vm) : null), - t.div({className: "footer"}, [ - t.p(t.img({src: "images/matrix-logo.svg"})), - t.p(["This invite uses ", externalLink(t, "https://matrix.org", "Matrix"), ", an open network for secure, decentralized communication."]), - t.ul({className: "links"}, [ - t.li(externalLink(t, "https://github.com/matrix-org/matrix.to", "GitHub project")), - t.li(externalLink(t, "https://github.com/matrix-org/matrix.to/tree/main/src/open/clients", "Add your app")), - t.li({className: {hidden: vm => !vm.hasPreferences}}, - t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")), - ]) - ]) - ]); - } + t.div({className: "footer"}, [ + t.p(t.img({src: "images/matrix-logo.svg"})), + t.p(["This invite uses ", externalLink(t, "https://matrix.org", "Matrix"), ", an open network for secure, decentralized communication."]), + t.ul({className: "links"}, [ + t.li(externalLink(t, "https://github.com/matrix-org/matrix.to", "GitHub project")), + t.li(externalLink(t, "https://github.com/matrix-org/matrix.to/tree/main/src/open/clients", "Add your app")), + t.li({className: {hidden: vm => !vm.hasPreferences}}, + t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")), + ]) + ]) + ]); + } } function externalLink(t, href, label) { - return t.a({href, target: "_blank", rel: "noopener noreferrer"}, label); + return t.a({href, target: "_blank", rel: "noopener noreferrer"}, label); } diff --git a/src/RootViewModel.js b/src/RootViewModel.js index a77b82b73fae4af9817d807a285cd4cb89c169ac..6c05cc9cf1032dcb260a1566e36712d9d53fcfb9 100644 --- a/src/RootViewModel.js +++ b/src/RootViewModel.js @@ -23,51 +23,51 @@ import {LoadServerPolicyViewModel} from "./policy/LoadServerPolicyViewModel.js"; import {Platform} from "./Platform.js"; export class RootViewModel extends ViewModel { - constructor(options) { - super(options); - this.link = null; - this.openLinkViewModel = null; - this.createLinkViewModel = null; + constructor(options) { + super(options); + this.link = null; + this.openLinkViewModel = null; + this.createLinkViewModel = null; this.loadServerPolicyViewModel = null; this.preferences.on("canClear", () => { this.emitChange(); }); - } + } - _updateChildVMs(oldLink) { - if (this.link) { - this.createLinkViewModel = null; - if (!oldLink || !oldLink.equals(this.link)) { - this.openLinkViewModel = new OpenLinkViewModel(this.childOptions({ - link: this.link, - clients: createClients(), - })); - } - } else { - this.openLinkViewModel = null; - this.createLinkViewModel = new CreateLinkViewModel(this.childOptions()); - } - this.emitChange(); - } + _updateChildVMs(oldLink) { + if (this.link) { + this.createLinkViewModel = null; + if (!oldLink || !oldLink.equals(this.link)) { + this.openLinkViewModel = new OpenLinkViewModel(this.childOptions({ + link: this.link, + clients: createClients(), + })); + } + } else { + this.openLinkViewModel = null; + this.createLinkViewModel = new CreateLinkViewModel(this.childOptions()); + } + this.emitChange(); + } - updateHash(hash) { + updateHash(hash) { if (hash.startsWith("#/policy/")) { const server = hash.substr(9); this.loadServerPolicyViewModel = new LoadServerPolicyViewModel(this.childOptions({server})); this.loadServerPolicyViewModel.load(); } else { - const oldLink = this.link; - this.link = Link.parse(hash); - this._updateChildVMs(oldLink); + const oldLink = this.link; + this.link = Link.parse(hash); + this._updateChildVMs(oldLink); } - } + } - clearPreferences() { - this.preferences.clear(); - this._updateChildVMs(); - } + clearPreferences() { + this.preferences.clear(); + this._updateChildVMs(); + } - get hasPreferences() { - return this.preferences.canClear; - } + get hasPreferences() { + return this.preferences.canClear; + } } diff --git a/src/create/CreateLinkView.js b/src/create/CreateLinkView.js index 784cb6610019102096b5d2d2cb6afc0319d28c8e..cd01322279ba140b5642d49529ba36eabde52769 100644 --- a/src/create/CreateLinkView.js +++ b/src/create/CreateLinkView.js @@ -19,31 +19,31 @@ import {PreviewView} from "../preview/PreviewView.js"; import {copyButton} from "../utils/copy.js"; export class CreateLinkView extends TemplateView { - render(t, vm) { + render(t, vm) { const link = t.a({href: vm => vm.linkUrl}, vm => vm.linkUrl); - return t.div({className: "CreateLinkView card"}, [ - t.h1("Create shareable links to Matrix rooms, users or messages without being tied to any app"), - t.form({action: "#", onSubmit: evt => this._onSubmit(evt)}, [ - t.div(t.input({ - className: "fullwidth large", - type: "text", - name: "identifier", + return t.div({className: "CreateLinkView card"}, [ + t.h1("Create shareable links to Matrix rooms, users or messages without being tied to any app"), + t.form({action: "#", onSubmit: evt => this._onSubmit(evt)}, [ + t.div(t.input({ + className: "fullwidth large", + type: "text", + name: "identifier", required: true, - placeholder: "#room:example.com, @user:example.com", + placeholder: "#room:example.com, @user:example.com", onChange: evt => this._onIdentifierChange(evt) - })), - t.div(t.input({className: "primary fullwidth icon link", type: "submit", value: "Create link"})) - ]), - ]); - } + })), + t.div(t.input({className: "primary fullwidth icon link", type: "submit", value: "Create link"})) + ]), + ]); + } - _onSubmit(evt) { - evt.preventDefault(); - const form = evt.target; - const {identifier} = form.elements; - this.value.createLink(identifier.value); + _onSubmit(evt) { + evt.preventDefault(); + const form = evt.target; + const {identifier} = form.elements; + this.value.createLink(identifier.value); identifier.value = ""; - } + } _onIdentifierChange(evt) { const inputField = evt.target; diff --git a/src/create/CreateLinkViewModel.js b/src/create/CreateLinkViewModel.js index 21f4c81b2433786c9fb5acfa4f36b3476cfe01ea..50aa8805659a80ed90a1d2425675fda2ca234546 100644 --- a/src/create/CreateLinkViewModel.js +++ b/src/create/CreateLinkViewModel.js @@ -19,11 +19,11 @@ import {PreviewViewModel} from "../preview/PreviewViewModel.js"; import {Link} from "../Link.js"; export class CreateLinkViewModel extends ViewModel { - constructor(options) { - super(options); + constructor(options) { + super(options); this._link = null; - this.previewViewModel = null; - } + this.previewViewModel = null; + } validateIdentifier(identifier) { return Link.validateIdentifier(identifier); diff --git a/src/main.js b/src/main.js index defe9ef94f3102fc5d7a81196ee8c944d56a2e29..9826dfd52f93b151166755ab0e2f7900286bb346 100644 --- a/src/main.js +++ b/src/main.js @@ -21,18 +21,18 @@ import {Preferences} from "./Preferences.js"; import {guessApplicablePlatforms} from "./Platform.js"; export async function main(container) { - const vm = new RootViewModel({ - request: xhrRequest, - openLink: url => location.href = url, - platforms: guessApplicablePlatforms(navigator.userAgent, navigator.platform), - preferences: new Preferences(window.localStorage), - origin: location.origin, - }); - vm.updateHash(decodeURIComponent(location.hash)); - window.__rootvm = vm; - const view = new RootView(vm); - container.appendChild(view.mount()); - window.addEventListener('hashchange', () => { - vm.updateHash(decodeURIComponent(location.hash)); - }); + const vm = new RootViewModel({ + request: xhrRequest, + openLink: url => location.href = url, + platforms: guessApplicablePlatforms(navigator.userAgent, navigator.platform), + preferences: new Preferences(window.localStorage), + origin: location.origin, + }); + vm.updateHash(decodeURIComponent(location.hash)); + window.__rootvm = vm; + const view = new RootView(vm); + container.appendChild(view.mount()); + window.addEventListener('hashchange', () => { + vm.updateHash(decodeURIComponent(location.hash)); + }); } diff --git a/src/open/ClientListView.js b/src/open/ClientListView.js index 8834cdf1d58aa430d4d7ba32f5d26641c4fdfd73..cf90f6cd822245baf63a4352c7693388a6a07898 100644 --- a/src/open/ClientListView.js +++ b/src/open/ClientListView.js @@ -18,50 +18,50 @@ import {TemplateView} from "../utils/TemplateView.js"; import {ClientView} from "./ClientView.js"; export class ClientListView extends TemplateView { - render(t, vm) { - return t.mapView(vm => vm.clientViewModel, () => { - if (vm.clientViewModel) { - return new ContinueWithClientView(vm); - } else { - return new AllClientsView(vm); - } - }); - } + render(t, vm) { + return t.mapView(vm => vm.clientViewModel, () => { + if (vm.clientViewModel) { + return new ContinueWithClientView(vm); + } else { + return new AllClientsView(vm); + } + }); + } } class AllClientsView extends TemplateView { - render(t, vm) { - return t.div({className: "ClientListView"}, [ - t.h2("Choose an app to continue"), - t.map(vm => vm.clientList, (clientList, t) => { - return t.div({className: "list"}, clientList.map(clientViewModel => { - return t.view(new ClientView(clientViewModel)); - })); - }), - t.div(t.label([ - t.input({ - type: "checkbox", - checked: vm.showUnsupportedPlatforms, - onChange: evt => vm.showUnsupportedPlatforms = evt.target.checked, - }), - "Show apps not available on my platform" - ])), - t.div(t.label({className: "filterOption"}, [ - t.input({ - type: "checkbox", - checked: vm.showExperimental, - onChange: evt => vm.showExperimental = evt.target.checked, - }), - "Show experimental apps" - ])), - ]); - } + render(t, vm) { + return t.div({className: "ClientListView"}, [ + t.h2("Choose an app to continue"), + t.map(vm => vm.clientList, (clientList, t) => { + return t.div({className: "list"}, clientList.map(clientViewModel => { + return t.view(new ClientView(clientViewModel)); + })); + }), + t.div(t.label([ + t.input({ + type: "checkbox", + checked: vm.showUnsupportedPlatforms, + onChange: evt => vm.showUnsupportedPlatforms = evt.target.checked, + }), + "Show apps not available on my platform" + ])), + t.div(t.label({className: "filterOption"}, [ + t.input({ + type: "checkbox", + checked: vm.showExperimental, + onChange: evt => vm.showExperimental = evt.target.checked, + }), + "Show experimental apps" + ])), + ]); + } } class ContinueWithClientView extends TemplateView { - render(t, vm) { - return t.div({className: "ClientListView"}, [ - t.div({className: "list"}, t.view(new ClientView(vm.clientViewModel))) - ]); - } + render(t, vm) { + return t.div({className: "ClientListView"}, [ + t.div({className: "list"}, t.view(new ClientView(vm.clientViewModel))) + ]); + } } diff --git a/src/open/ClientListViewModel.js b/src/open/ClientListViewModel.js index 626d720e60d8fcaec14aa4c1021685ebfeb052df..59e8364f3f223e478f703b2542575870de3ea176 100644 --- a/src/open/ClientListViewModel.js +++ b/src/open/ClientListViewModel.js @@ -20,70 +20,70 @@ import {ClientViewModel} from "./ClientViewModel.js"; import {ViewModel} from "../utils/ViewModel.js"; export class ClientListViewModel extends ViewModel { - constructor(options) { - super(options); - const {clients, client, link} = options; - this._clients = clients; - this._link = link; - this.clientList = null; - this._showExperimental = false; - this._showUnsupportedPlatforms = false; - this._filterClients(); - this.clientViewModel = null; - if (client) { - this._pickClient(client); - } - } + constructor(options) { + super(options); + const {clients, client, link} = options; + this._clients = clients; + this._link = link; + this.clientList = null; + this._showExperimental = false; + this._showUnsupportedPlatforms = false; + this._filterClients(); + this.clientViewModel = null; + if (client) { + this._pickClient(client); + } + } - get showUnsupportedPlatforms() { - return this._showUnsupportedPlatforms; - } + get showUnsupportedPlatforms() { + return this._showUnsupportedPlatforms; + } - get showExperimental() { - return this._showExperimental; - } + get showExperimental() { + return this._showExperimental; + } - set showUnsupportedPlatforms(enabled) { - this._showUnsupportedPlatforms = enabled; - this._filterClients(); - } + set showUnsupportedPlatforms(enabled) { + this._showUnsupportedPlatforms = enabled; + this._filterClients(); + } - set showExperimental(enabled) { - this._showExperimental = enabled; - this._filterClients(); - } + set showExperimental(enabled) { + this._showExperimental = enabled; + this._filterClients(); + } - _filterClients() { - const clientVMs = this._clients.filter(client => { + _filterClients() { + const clientVMs = this._clients.filter(client => { const platformMaturities = this.platforms.map(p => client.getMaturity(p)); - const isStable = platformMaturities.includes(Maturity.Stable) || platformMaturities.includes(Maturity.Beta); - const isSupported = client.platforms.some(p => this.platforms.includes(p)); - if (!this._showExperimental && !isStable) { - return false; - } - if (!this._showUnsupportedPlatforms && !isSupported) { - return false; - } - return true; - }).map(client => new ClientViewModel(this.childOptions({ - client, - link: this._link, - pickClient: client => this._pickClient(client) - }))); + const isStable = platformMaturities.includes(Maturity.Stable) || platformMaturities.includes(Maturity.Beta); + const isSupported = client.platforms.some(p => this.platforms.includes(p)); + if (!this._showExperimental && !isStable) { + return false; + } + if (!this._showUnsupportedPlatforms && !isSupported) { + return false; + } + return true; + }).map(client => new ClientViewModel(this.childOptions({ + client, + link: this._link, + pickClient: client => this._pickClient(client) + }))); const preferredClientVMs = clientVMs.filter(c => c.hasPreferredWebInstance); const otherClientVMs = clientVMs.filter(c => !c.hasPreferredWebInstance); this.clientList = preferredClientVMs.concat(otherClientVMs); - this.emitChange(); - } + this.emitChange(); + } - _pickClient(client) { - this.clientViewModel = this.clientList.find(vm => vm.clientId === client.id); + _pickClient(client) { + this.clientViewModel = this.clientList.find(vm => vm.clientId === client.id); this.clientViewModel.pick(this); - this.emitChange(); - } + this.emitChange(); + } - showAll() { - this.clientViewModel = null; - this.emitChange(); - } + showAll() { + this.clientViewModel = null; + this.emitChange(); + } } diff --git a/src/open/ClientView.js b/src/open/ClientView.js index 9b0eabca7173afa3dee24e135bc217e538ac8a0f..76c73e9f6626c8dff5b580bc2f54c4f95d3a8f78 100644 --- a/src/open/ClientView.js +++ b/src/open/ClientView.js @@ -19,11 +19,11 @@ import {copy} from "../utils/copy.js"; import {text, tag} from "../utils/html.js"; function formatPlatforms(platforms) { - return platforms.reduce((str, p, i, all) => { - const first = i === 0; - const last = i === all.length - 1; - return str + (first ? "" : last ? " & " : ", ") + p; - }, ""); + return platforms.reduce((str, p, i, all) => { + const first = i === 0; + const last = i === all.length - 1; + return str + (first ? "" : last ? " & " : ", ") + p; + }, ""); } function renderInstructions(parts) { @@ -38,46 +38,46 @@ function renderInstructions(parts) { export class ClientView extends TemplateView { - render(t, vm) { - return t.div({className: {"ClientView": true, "isPreferred": vm => vm.hasPreferredWebInstance}}, [ + render(t, vm) { + return t.div({className: {"ClientView": true, "isPreferred": vm => vm.hasPreferredWebInstance}}, [ ... vm.hasPreferredWebInstance ? [t.div({className: "hostedBanner"}, vm.hostedByBannerLabel)] : [], - t.div({className: "header"}, [ - t.div({className: "description"}, [ - t.h3(vm.name), - t.p([vm.description, " ", t.a({ + t.div({className: "header"}, [ + t.div({className: "description"}, [ + t.h3(vm.name), + t.p([vm.description, " ", t.a({ href: vm.homepage, target: "_blank", rel: "noopener noreferrer" }, "Learn more")]), - t.p({className: "platforms"}, formatPlatforms(vm.availableOnPlatformNames)), - ]), - t.img({className: "clientIcon", src: vm.iconUrl}) - ]), + t.p({className: "platforms"}, formatPlatforms(vm.availableOnPlatformNames)), + ]), + t.img({className: "clientIcon", src: vm.iconUrl}) + ]), t.mapView(vm => vm.stage, stage => { switch (stage) { case "open": return new OpenClientView(vm); case "install": return new InstallClientView(vm); } }), - ]); - } + ]); + } } class OpenClientView extends TemplateView { - render(t, vm) { - return t.div({className: "OpenClientView"}, [ - ...vm.openActions.map(a => renderAction(t, a)), + render(t, vm) { + return t.div({className: "OpenClientView"}, [ + ...vm.openActions.map(a => renderAction(t, a)), showBack(t, vm), - ]); - } + ]); + } } class InstallClientView extends TemplateView { - render(t, vm) { - const children = []; + render(t, vm) { + const children = []; const textInstructions = vm.textInstructions; - if (textInstructions) { + if (textInstructions) { const copyButton = t.button({ className: "copy", title: "Copy instructions", @@ -91,25 +91,25 @@ class InstallClientView extends TemplateView { } } }); - children.push(t.p({className: "instructions"}, renderInstructions(textInstructions).concat(copyButton))); - } + children.push(t.p({className: "instructions"}, renderInstructions(textInstructions).concat(copyButton))); + } - const actions = t.div({className: "actions"}, vm.installActions.map(a => renderAction(t, a))); - children.push(actions); + const actions = t.div({className: "actions"}, vm.installActions.map(a => renderAction(t, a))); + children.push(actions); - if (vm.showDeepLinkInInstall) { - const openItHere = t.a({ - rel: "noopener noreferrer", - href: vm.openActions[0].url, - onClick: () => vm.openActions[0].activated(), - }, "open it here"); - children.push(t.p([`If you already have ${vm.name} installed, you can `, openItHere, "."])) - } + if (vm.showDeepLinkInInstall) { + const openItHere = t.a({ + rel: "noopener noreferrer", + href: vm.openActions[0].url, + onClick: () => vm.openActions[0].activated(), + }, "open it here"); + children.push(t.p([`If you already have ${vm.name} installed, you can `, openItHere, "."])) + } children.push(showBack(t, vm)); - return t.div({className: "InstallClientView"}, children); - } + return t.div({className: "InstallClientView"}, children); + } } function showBack(t, vm) { diff --git a/src/open/ClientViewModel.js b/src/open/ClientViewModel.js index 3297abef293637726a7b7467ce845e8fb504fe4e..398731532f0916085f8d4c41664cd483292e8b6a 100644 --- a/src/open/ClientViewModel.js +++ b/src/open/ClientViewModel.js @@ -27,28 +27,28 @@ function getMatchingPlatforms(client, supportedPlatforms) { } export class ClientViewModel extends ViewModel { - constructor(options) { - super(options); - const {client, link, pickClient} = options; - this._client = client; - this._link = link; - this._pickClient = pickClient; + constructor(options) { + super(options); + const {client, link, pickClient} = options; + this._client = client; + this._link = link; + this._pickClient = pickClient; // to provide "choose other client" button after calling pick() this._clientListViewModel = null; this._update(); - } + } _update() { - const matchingPlatforms = getMatchingPlatforms(this._client, this.platforms); - this._webPlatform = matchingPlatforms.find(p => isWebPlatform(p)); - this._nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p)); + const matchingPlatforms = getMatchingPlatforms(this._client, this.platforms); + this._webPlatform = matchingPlatforms.find(p => isWebPlatform(p)); + this._nativePlatform = matchingPlatforms.find(p => !isWebPlatform(p)); const preferredPlatform = matchingPlatforms.find(p => p === this.preferences.platform); - this._proposedPlatform = preferredPlatform || this._nativePlatform || this._webPlatform; + this._proposedPlatform = preferredPlatform || this._nativePlatform || this._webPlatform; this.openActions = this._createOpenActions(); - this.installActions = this._createInstallActions(); - this._clientCanIntercept = !!(this._nativePlatform && this._client.canInterceptMatrixToLinks(this._nativePlatform)); - this._showOpen = this.openActions.length && !this._clientCanIntercept; + this.installActions = this._createInstallActions(); + this._clientCanIntercept = !!(this._nativePlatform && this._client.canInterceptMatrixToLinks(this._nativePlatform)); + this._showOpen = this.openActions.length && !this._clientCanIntercept; } // these are only shown in the open stage @@ -93,40 +93,40 @@ export class ClientViewModel extends ViewModel { } // these are only shown in the install stage - _createInstallActions() { - let actions = []; - if (this._nativePlatform) { - const nativeActions = (this._client.getInstallLinks(this._nativePlatform) || []).map(installLink => { - return { - label: installLink.getDescription(this._nativePlatform), - url: installLink.createInstallURL(this._link), - kind: installLink.channelId, - primary: true, - activated: () => this.preferences.setClient(this._client.id, this._nativePlatform), - }; - }); - actions.push(...nativeActions); - } - if (this._webPlatform) { - const webDeepLink = this._client.getDeepLink(this._webPlatform, this._link); - if (webDeepLink) { + _createInstallActions() { + let actions = []; + if (this._nativePlatform) { + const nativeActions = (this._client.getInstallLinks(this._nativePlatform) || []).map(installLink => { + return { + label: installLink.getDescription(this._nativePlatform), + url: installLink.createInstallURL(this._link), + kind: installLink.channelId, + primary: true, + activated: () => this.preferences.setClient(this._client.id, this._nativePlatform), + }; + }); + actions.push(...nativeActions); + } + if (this._webPlatform) { + const webDeepLink = this._client.getDeepLink(this._webPlatform, this._link); + if (webDeepLink) { const webLabel = this.hasPreferredWebInstance ? `Open on ${this._client.getPreferredWebInstance(this._link)}` : `Continue in your browser`; - actions.push({ - label: webLabel, - url: webDeepLink, - kind: "open-in-web", - activated: () => { + actions.push({ + label: webLabel, + url: webDeepLink, + kind: "open-in-web", + activated: () => { if (!this.hasPreferredWebInstance) { this.preferences.setClient(this._client.id, this._webPlatform); } }, - }); - } - } - return actions; - } + }); + } + } + return actions; + } get hasPreferredWebInstance() { // also check there is a web platform that matches the platforms the user is on (mobile or desktop web) @@ -150,17 +150,17 @@ export class ClientViewModel extends ViewModel { return this._client.homepage; } - get identifier() { - return this._link.identifier; - } + get identifier() { + return this._link.identifier; + } - get description() { - return this._client.description; - } + get description() { + return this._client.description; + } - get clientId() { - return this._client.id; - } + get clientId() { + return this._client.id; + } get name() { return this._client.name; @@ -174,44 +174,44 @@ export class ClientViewModel extends ViewModel { return this._showOpen ? "open" : "install"; } - get textInstructions() { + get textInstructions() { let instructions = this._client.getLinkInstructions(this._proposedPlatform, this._link); if (instructions && !Array.isArray(instructions)) { instructions = [instructions]; } - return instructions; - } + return instructions; + } get copyString() { return this._client.getCopyString(this._proposedPlatform, this._link); } - get showDeepLinkInInstall() { + get showDeepLinkInInstall() { // we can assume this._nativePlatform as this._clientCanIntercept already checks it - return this._clientCanIntercept && !!this._client.getDeepLink(this._nativePlatform, this._link); - } - - get availableOnPlatformNames() { - const platforms = this._client.platforms; - const textPlatforms = []; - const hasWebPlatform = platforms.some(p => isWebPlatform(p)); - if (hasWebPlatform) { - textPlatforms.push("Web"); - } - const desktopPlatforms = platforms.filter(p => isDesktopPlatform(p)); - if (desktopPlatforms.length === 1) { - textPlatforms.push(desktopPlatforms[0]); - } else { - textPlatforms.push("Desktop"); - } - if (platforms.includes(Platform.Android)) { - textPlatforms.push("Android"); - } - if (platforms.includes(Platform.iOS)) { - textPlatforms.push("iOS"); - } - return textPlatforms; - } + return this._clientCanIntercept && !!this._client.getDeepLink(this._nativePlatform, this._link); + } + + get availableOnPlatformNames() { + const platforms = this._client.platforms; + const textPlatforms = []; + const hasWebPlatform = platforms.some(p => isWebPlatform(p)); + if (hasWebPlatform) { + textPlatforms.push("Web"); + } + const desktopPlatforms = platforms.filter(p => isDesktopPlatform(p)); + if (desktopPlatforms.length === 1) { + textPlatforms.push(desktopPlatforms[0]); + } else { + textPlatforms.push("Desktop"); + } + if (platforms.includes(Platform.Android)) { + textPlatforms.push("Android"); + } + if (platforms.includes(Platform.iOS)) { + textPlatforms.push("iOS"); + } + return textPlatforms; + } pick(clientListViewModel) { this._clientListViewModel = clientListViewModel; diff --git a/src/open/OpenLinkView.js b/src/open/OpenLinkView.js index 8a2dbd45aba559cce0aa4518ebf5fe5b3d1679ec..c7dfe0fae85f877790ffe1b899fc293db32347f0 100644 --- a/src/open/OpenLinkView.js +++ b/src/open/OpenLinkView.js @@ -20,14 +20,14 @@ import {PreviewView} from "../preview/PreviewView.js"; import {ServerConsentView} from "./ServerConsentView.js"; export class OpenLinkView extends TemplateView { - render(t, vm) { - return t.div({className: "OpenLinkView card"}, [ - t.mapView(vm => vm.previewViewModel, previewVM => previewVM ? + render(t, vm) { + return t.div({className: "OpenLinkView card"}, [ + t.mapView(vm => vm.previewViewModel, previewVM => previewVM ? new ShowLinkView(vm) : new ServerConsentView(vm.serverConsentViewModel) ), - ]); - } + ]); + } } class ShowLinkView extends TemplateView { diff --git a/src/open/OpenLinkViewModel.js b/src/open/OpenLinkViewModel.js index ebd62107d3d50a3e5f54d476609344e27fa5481f..2b5d2954ac940d0a03cdc22f82f385d35a383a09 100644 --- a/src/open/OpenLinkViewModel.js +++ b/src/open/OpenLinkViewModel.js @@ -23,21 +23,21 @@ import {getLabelForLinkKind} from "../Link.js"; import {orderedUnique} from "../utils/unique.js"; export class OpenLinkViewModel extends ViewModel { - constructor(options) { - super(options); - const {clients, link} = options; - this._link = link; - this._clients = clients; + constructor(options) { + super(options); + const {clients, link} = options; + this._link = link; + this._clients = clients; this.serverConsentViewModel = null; - this.previewViewModel = null; + this.previewViewModel = null; this.clientsViewModel = null; - this.previewLoading = false; + this.previewLoading = false; if (this.preferences.homeservers === null) { this._showServerConsent(); } else { this._showLink(); } - } + } _showServerConsent() { let servers = []; @@ -67,24 +67,24 @@ export class OpenLinkViewModel extends ViewModel { link: this._link, consentedServers: this.preferences.homeservers })); - this.previewLoading = true; - this.emitChange(); - await this.previewViewModel.load(); - this.previewLoading = false; - this.emitChange(); + this.previewLoading = true; + this.emitChange(); + await this.previewViewModel.load(); + this.previewLoading = false; + this.emitChange(); } - get previewDomain() { - return this.previewViewModel?.domain; - } + get previewDomain() { + return this.previewViewModel?.domain; + } get previewFailed() { return this.previewViewModel?.failed; } - get showClientsLabel() { - return getLabelForLinkKind(this._link.kind); - } + get showClientsLabel() { + return getLabelForLinkKind(this._link.kind); + } changeServer() { this.previewViewModel = null; diff --git a/src/open/ServerConsentView.js b/src/open/ServerConsentView.js index e35c2579e03b4cbb56d236c83683e5c9752a15e4..c308e904b13471743d3ef19cbc5495f608ddbc56 100644 --- a/src/open/ServerConsentView.js +++ b/src/open/ServerConsentView.js @@ -27,7 +27,7 @@ export class ServerConsentView extends TemplateView { className: "text", onClick: () => vm.continueWithoutConsent(this._askEveryTimeChecked) }, "continue without a preview"); - return t.div({className: "ServerConsentView"}, [ + return t.div({className: "ServerConsentView"}, [ t.p([ "Preview this link using the ", t.strong(vm => vm.selectedServer || "…"), @@ -56,7 +56,7 @@ export class ServerConsentView extends TemplateView { ]) ]) ]); - } + } _onSubmit(evt) { evt.preventDefault(); diff --git a/src/open/ServerConsentViewModel.js b/src/open/ServerConsentViewModel.js index 2cb6e070958e36acb193963f734b224e0ae4d00e..25564eef6060c7a5319df2df0195da375c481a59 100644 --- a/src/open/ServerConsentViewModel.js +++ b/src/open/ServerConsentViewModel.js @@ -22,13 +22,13 @@ import {getLabelForLinkKind} from "../Link.js"; import {orderedUnique} from "../utils/unique.js"; export class ServerConsentViewModel extends ViewModel { - constructor(options) { - super(options); + constructor(options) { + super(options); this.servers = options.servers; this.done = options.done; this.selectedServer = this.servers[0]; this.showSelectServer = false; - } + } setShowServers() { this.showSelectServer = true; diff --git a/src/open/clients/Element.js b/src/open/clients/Element.js index 57d092d62a0d2b95423da2f742a15c12b9b7edc2..4cb235a1cf448f567ac6bda2908d0b6f8cfcbd69 100644 --- a/src/open/clients/Element.js +++ b/src/open/clients/Element.js @@ -15,7 +15,7 @@ limitations under the License. */ import {Maturity, Platform, LinkKind, - FDroidLink, AppleStoreLink, PlayStoreLink, WebsiteLink} from "../types.js"; + FDroidLink, AppleStoreLink, PlayStoreLink, WebsiteLink} from "../types.js"; const trustedWebInstances = [ "app.element.io", // first one is the default one @@ -29,69 +29,69 @@ const trustedWebInstances = [ * Information on how to deep link to a given matrix client. */ export class Element { - get id() { return "element.io"; } + get id() { return "element.io"; } - get platforms() { - return [ - Platform.Android, Platform.iOS, - Platform.Windows, Platform.macOS, Platform.Linux, - Platform.DesktopWeb - ]; - } + get platforms() { + return [ + Platform.Android, Platform.iOS, + Platform.Windows, Platform.macOS, Platform.Linux, + Platform.DesktopWeb + ]; + } get icon() { return "images/client-icons/element.svg"; } get appleAssociatedAppId() { return "7J4U792NQT.im.vector.app"; } - get name() {return "Element"; } - get description() { return 'Fully-featured Matrix client, used by millions.'; } - get homepage() { return "https://element.io"; } - get author() { return "Element"; } - getMaturity(platform) { return Maturity.Stable; } + get name() {return "Element"; } + get description() { return 'Fully-featured Matrix client, used by millions.'; } + get homepage() { return "https://element.io"; } + get author() { return "Element"; } + getMaturity(platform) { return Maturity.Stable; } - getDeepLink(platform, link) { - let fragmentPath; - switch (link.kind) { - case LinkKind.User: - fragmentPath = `user/${link.identifier}`; - break; - case LinkKind.Room: - fragmentPath = `room/${link.identifier}`; - break; - case LinkKind.Group: - fragmentPath = `group/${link.identifier}`; - break; - case LinkKind.Event: - fragmentPath = `room/${link.identifier}/${link.eventId}`; - break; - } + getDeepLink(platform, link) { + let fragmentPath; + switch (link.kind) { + case LinkKind.User: + fragmentPath = `user/${link.identifier}`; + break; + case LinkKind.Room: + fragmentPath = `room/${link.identifier}`; + break; + case LinkKind.Group: + fragmentPath = `group/${link.identifier}`; + break; + case LinkKind.Event: + fragmentPath = `room/${link.identifier}/${link.eventId}`; + break; + } const isWebPlatform = platform === Platform.DesktopWeb || platform === Platform.MobileWeb; - if (isWebPlatform || platform === Platform.iOS) { + if (isWebPlatform || platform === Platform.iOS) { let instanceHost = trustedWebInstances[0]; // we use app.element.io which iOS will intercept, but it likely won't intercept any other trusted instances // so only use a preferred web instance for true web links. if (isWebPlatform && trustedWebInstances.includes(link.webInstances[this.id])) { instanceHost = link.webInstances[this.id]; } - return `https://${instanceHost}/#/${fragmentPath}`; - } else if (platform === Platform.Linux || platform === Platform.Windows || platform === Platform.macOS) { - return `element://vector/webapp/#/${fragmentPath}`; - } else { + return `https://${instanceHost}/#/${fragmentPath}`; + } else if (platform === Platform.Linux || platform === Platform.Windows || platform === Platform.macOS) { + return `element://vector/webapp/#/${fragmentPath}`; + } else { return `element://${fragmentPath}`; } - } + } - getLinkInstructions(platform, link) {} + getLinkInstructions(platform, link) {} getCopyString(platform, link) {} - getInstallLinks(platform) { - switch (platform) { - case Platform.iOS: return [new AppleStoreLink('vector', 'id1083446067')]; - case Platform.Android: return [new PlayStoreLink('im.vector.app'), new FDroidLink('im.vector.app')]; - default: return [new WebsiteLink("https://element.io/get-started")]; - } - } + getInstallLinks(platform) { + switch (platform) { + case Platform.iOS: return [new AppleStoreLink('vector', 'id1083446067')]; + case Platform.Android: return [new PlayStoreLink('im.vector.app'), new FDroidLink('im.vector.app')]; + default: return [new WebsiteLink("https://element.io/get-started")]; + } + } - canInterceptMatrixToLinks(platform) { - return platform === Platform.Android; - } + canInterceptMatrixToLinks(platform) { + return platform === Platform.Android; + } getPreferredWebInstance(link) { const idx = trustedWebInstances.indexOf(link.webInstances[this.id]) diff --git a/src/open/clients/Fractal.js b/src/open/clients/Fractal.js index 2f6e85630d16403bf1b6f7baa656bd10e56e6065..2187724f603d292a3ce8bbf3d0c3a7aefafa9afe 100644 --- a/src/open/clients/Fractal.js +++ b/src/open/clients/Fractal.js @@ -20,22 +20,22 @@ import {Maturity, Platform, LinkKind, FlathubLink} from "../types.js"; * Information on how to deep link to a given matrix client. */ export class Fractal { - get id() { return "fractal"; } - get name() { return "Fractal"; } + get id() { return "fractal"; } + get name() { return "Fractal"; } get icon() { return "images/client-icons/fractal.png"; } get author() { return "Daniel Garcia Moreno"; } get homepage() { return "https://gitlab.gnome.org/GNOME/fractal"; } - get platforms() { return [Platform.Linux]; } - get description() { return 'Fractal is a Matrix Client written in Rust.'; } - getMaturity(platform) { return Maturity.Beta; } - getDeepLink(platform, link) {} - canInterceptMatrixToLinks(platform) { return false; } + get platforms() { return [Platform.Linux]; } + get description() { return 'Fractal is a Matrix Client written in Rust.'; } + getMaturity(platform) { return Maturity.Beta; } + getDeepLink(platform, link) {} + canInterceptMatrixToLinks(platform) { return false; } - getLinkInstructions(platform, link) { + getLinkInstructions(platform, link) { if (link.kind === LinkKind.User || link.kind === LinkKind.Room) { return "Click the '+' button in the top right and paste the identifier"; } - } + } getCopyString(platform, link) { if (link.kind === LinkKind.User || link.kind === LinkKind.Room) { @@ -43,7 +43,7 @@ export class Fractal { } } - getInstallLinks(platform) { + getInstallLinks(platform) { if (platform === Platform.Linux) { return [new FlathubLink("org.gnome.Fractal")]; } diff --git a/src/open/clients/Nheko.js b/src/open/clients/Nheko.js index 06a4ddbb2d56d7fb5cc5c644c8b3e521207dd903..c2e3ab0a88a24875aa26a32a784ef25728502dca 100644 --- a/src/open/clients/Nheko.js +++ b/src/open/clients/Nheko.js @@ -20,49 +20,49 @@ import {Maturity, Platform, LinkKind, FlathubLink, style} from "../types.js"; * Information on how to deep link to a given matrix client. */ export class Nheko { - get id() { return "nheko"; } - get name() { return "Nheko"; } + get id() { return "nheko"; } + get name() { return "Nheko"; } get icon() { return "images/client-icons/nheko.svg"; } get author() { return "mujx, red_sky, deepbluev7, Konstantinos Sideris"; } get homepage() { return "https://github.com/Nheko-Reborn/nheko"; } - get platforms() { return [Platform.Windows, Platform.macOS, Platform.Linux]; } - get description() { return 'A native desktop app for Matrix that feels more like a mainstream chat app.'; } - getMaturity(platform) { return Maturity.Beta; } - getDeepLink(platform, link) { - if (platform === Platform.Linux || platform === Platform.Windows) { - let identifier = encodeURIComponent(link.identifier.substring(1)); - let isRoomid = link.identifier.substring(0, 1) === '!'; - let fragmentPath; - switch (link.kind) { - case LinkKind.User: - fragmentPath = `u/${identifier}?action=chat`; - break; - case LinkKind.Room: - case LinkKind.Event: - if (isRoomid) - fragmentPath = `roomid/${identifier}`; - else - fragmentPath = `r/${identifier}`; + get platforms() { return [Platform.Windows, Platform.macOS, Platform.Linux]; } + get description() { return 'A native desktop app for Matrix that feels more like a mainstream chat app.'; } + getMaturity(platform) { return Maturity.Beta; } + getDeepLink(platform, link) { + if (platform === Platform.Linux || platform === Platform.Windows) { + let identifier = encodeURIComponent(link.identifier.substring(1)); + let isRoomid = link.identifier.substring(0, 1) === '!'; + let fragmentPath; + switch (link.kind) { + case LinkKind.User: + fragmentPath = `u/${identifier}?action=chat`; + break; + case LinkKind.Room: + case LinkKind.Event: + if (isRoomid) + fragmentPath = `roomid/${identifier}`; + else + fragmentPath = `r/${identifier}`; - if (link.kind === LinkKind.Event) - fragmentPath += `/e/${encodeURIComponent(link.eventId.substring(1))}`; - fragmentPath += '?action=join'; - fragmentPath += link.servers.map(server => `&via=${encodeURIComponent(server)}`).join(''); - break; - case LinkKind.Group: - return; - } - return `matrix:${fragmentPath}`; - } - } - canInterceptMatrixToLinks(platform) { return false; } + if (link.kind === LinkKind.Event) + fragmentPath += `/e/${encodeURIComponent(link.eventId.substring(1))}`; + fragmentPath += '?action=join'; + fragmentPath += link.servers.map(server => `&via=${encodeURIComponent(server)}`).join(''); + break; + case LinkKind.Group: + return; + } + return `matrix:${fragmentPath}`; + } + } + canInterceptMatrixToLinks(platform) { return false; } - getLinkInstructions(platform, link) { - switch (link.kind) { - case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)]; - case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)]; - } - } + getLinkInstructions(platform, link) { + switch (link.kind) { + case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)]; + case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)]; + } + } getCopyString(platform, link) { switch (link.kind) { @@ -71,7 +71,7 @@ export class Nheko { } } - getInstallLinks(platform) { + getInstallLinks(platform) { if (platform === Platform.Linux) { return [new FlathubLink("io.github.NhekoReborn.Nheko")]; } diff --git a/src/open/clients/Weechat.js b/src/open/clients/Weechat.js index fae34ce3defea5b82856c6ca54b94882a043b11e..36fe6fb22dd8f48d7ac7006a25898c981531949f 100644 --- a/src/open/clients/Weechat.js +++ b/src/open/clients/Weechat.js @@ -20,23 +20,23 @@ import {Maturity, Platform, LinkKind, WebsiteLink, style} from "../types.js"; * Information on how to deep link to a given matrix client. */ export class Weechat { - get id() { return "weechat"; } - get name() { return "Weechat"; } + get id() { return "weechat"; } + get name() { return "Weechat"; } get icon() { return "images/client-icons/weechat.svg"; } get author() { return "Poljar"; } get homepage() { return "https://github.com/poljar/weechat-matrix"; } - get platforms() { return [Platform.Windows, Platform.macOS, Platform.Linux]; } - get description() { return 'Command-line Matrix interface using Weechat.'; } - getMaturity(platform) { return Maturity.Beta; } - getDeepLink(platform, link) {} - canInterceptMatrixToLinks(platform) { return false; } - - getLinkInstructions(platform, link) { - switch (link.kind) { - case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)]; - case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)]; - } - } + get platforms() { return [Platform.Windows, Platform.macOS, Platform.Linux]; } + get description() { return 'Command-line Matrix interface using Weechat.'; } + getMaturity(platform) { return Maturity.Beta; } + getDeepLink(platform, link) {} + canInterceptMatrixToLinks(platform) { return false; } + + getLinkInstructions(platform, link) { + switch (link.kind) { + case LinkKind.User: return [`Type `, style.code(`/invite ${link.identifier}`)]; + case LinkKind.Room: return [`Type `, style.code(`/join ${link.identifier}`)]; + } + } getCopyString(platform, link) { switch (link.kind) { @@ -45,7 +45,7 @@ export class Weechat { } } - getInstallLinks(platform) {} + getInstallLinks(platform) {} getPreferredWebInstance(link) {} } diff --git a/src/open/clients/index.js b/src/open/clients/index.js index 8dc7c254d848ef5117bffa8ef62d9a03236067a2..44acee2958333d7c4cef67f70a84076397dbe8ed 100644 --- a/src/open/clients/index.js +++ b/src/open/clients/index.js @@ -23,13 +23,13 @@ import {Tensor} from "./Tensor.js"; import {Fluffychat} from "./Fluffychat.js"; export function createClients() { - return [ - new Element(), - new Weechat(), - new Nheko(), - new Fractal(), - new Quaternion(), - new Tensor(), - new Fluffychat(), - ]; + return [ + new Element(), + new Weechat(), + new Nheko(), + new Fractal(), + new Quaternion(), + new Tensor(), + new Fluffychat(), + ]; } diff --git a/src/open/types.js b/src/open/types.js index eb99b2b43a4e1ec474612eb8a390a714eb7f852a..4263c619763ca68188bd3ec0e8a4e425eb012ea0 100644 --- a/src/open/types.js +++ b/src/open/types.js @@ -21,8 +21,8 @@ export {Platform} from "../Platform.js"; export class AppleStoreLink { constructor(org, appId) { - this._org = org; - this._appId = appId; + this._org = org; + this._appId = appId; } createInstallURL(link) { @@ -40,7 +40,7 @@ export class AppleStoreLink { export class PlayStoreLink { constructor(appId) { - this._appId = appId; + this._appId = appId; } createInstallURL(link) { @@ -58,7 +58,7 @@ export class PlayStoreLink { export class FDroidLink { constructor(appId) { - this._appId = appId; + this._appId = appId; } createInstallURL(link) { @@ -94,7 +94,7 @@ export class FlathubLink { export class WebsiteLink { constructor(url) { - this._url = url; + this._url = url; } createInstallURL(link) { diff --git a/src/policy/LoadServerPolicyView.js b/src/policy/LoadServerPolicyView.js index b6d2f089ff4291b6b06ca08b1478f6da1f098c1a..39cc25cdeefd5f1fce9ddefa10a50d40cf0692dc 100644 --- a/src/policy/LoadServerPolicyView.js +++ b/src/policy/LoadServerPolicyView.js @@ -17,10 +17,10 @@ limitations under the License. import {TemplateView} from "../utils/TemplateView.js"; export class LoadServerPolicyView extends TemplateView { - render(t, vm) { - return t.div({className: "LoadServerPolicyView card"}, [ - t.div({className: {spinner: true, hidden: vm => !vm.loading}}), + render(t, vm) { + return t.div({className: "LoadServerPolicyView card"}, [ + t.div({className: {spinner: true, hidden: vm => !vm.loading}}), t.h2(vm => vm.message) - ]); - } + ]); + } } diff --git a/src/policy/LoadServerPolicyViewModel.js b/src/policy/LoadServerPolicyViewModel.js index 4e6a11954adc52f94002f163670b1072b3f5b157..f6d2976701449d1684c221a94f2d27efcdd11b40 100644 --- a/src/policy/LoadServerPolicyViewModel.js +++ b/src/policy/LoadServerPolicyViewModel.js @@ -18,12 +18,12 @@ import {ViewModel} from "../utils/ViewModel.js"; import {resolveServer} from "../preview/HomeServer.js"; export class LoadServerPolicyViewModel extends ViewModel { - constructor(options) { - super(options); - this.server = options.server; + constructor(options) { + super(options); + this.server = options.server; this.message = `Looking up ${this.server} privacy policy…`; this.loading = false; - } + } async load() { this.loading = true; diff --git a/src/preview/HomeServer.js b/src/preview/HomeServer.js index 3cb91fc5f41d0c342a3715199fa99dc6cedadd68..964e1e7782cd272de65f2e98cd36445dc86eda51 100644 --- a/src/preview/HomeServer.js +++ b/src/preview/HomeServer.js @@ -20,61 +20,76 @@ function noTrailingSlash(url) { export async function resolveServer(request, baseURL) { baseURL = noTrailingSlash(baseURL); - if (!baseURL.startsWith("http://") && !baseURL.startsWith("https://")) { - baseURL = `https://${baseURL}`; - } - { - const {status, body} = await request(`${baseURL}/.well-known/matrix/client`, {method: "GET"}).response(); - if (status === 200) { - const proposedBaseURL = body?.['m.homeserver']?.base_url; - if (typeof proposedBaseURL === "string") { - baseURL = noTrailingSlash(proposedBaseURL); - } - } - } - { - const {status} = await request(`${baseURL}/_matrix/client/versions`, {method: "GET"}).response(); - if (status !== 200) { - throw new Error(`Invalid versions response from ${baseURL}`); - } - } - return new HomeServer(request, baseURL); + if (!baseURL.startsWith("http://") && !baseURL.startsWith("https://")) { + baseURL = `https://${baseURL}`; + } + { + try { + const {status, body} = await request(`${baseURL}/.well-known/matrix/client`, {method: "GET"}).response(); + if (status === 200) { + const proposedBaseURL = body?.['m.homeserver']?.base_url; + if (typeof proposedBaseURL === "string") { + baseURL = noTrailingSlash(proposedBaseURL); + } + } + } catch (e) { + console.warn("Failed to fetch ${baseURL}/.well-known/matrix/client", e); + } + } + { + const {status} = await request(`${baseURL}/_matrix/client/versions`, {method: "GET"}).response(); + if (status !== 200) { + throw new Error(`Invalid versions response from ${baseURL}`); + } + } + return new HomeServer(request, baseURL); } export class HomeServer { - constructor(request, baseURL) { - this._request = request; - this.baseURL = baseURL; - } + constructor(request, baseURL) { + this._request = request; + this.baseURL = baseURL; + } - async getUserProfile(userId) { - const {body} = await this._request(`${this.baseURL}/_matrix/client/r0/profile/${encodeURIComponent(userId)}`).response(); - return body; - } + async getUserProfile(userId) { + const {body} = await this._request(`${this.baseURL}/_matrix/client/r0/profile/${encodeURIComponent(userId)}`).response(); + return body; + } - async findPublicRoomById(roomId) { - const {body, status} = await this._request(`${this.baseURL}/_matrix/client/r0/directory/list/room/${encodeURIComponent(roomId)}`).response(); - if (status !== 200 || body.visibility !== "public") { - return; - } - let nextBatch; - do { - const queryParams = encodeQueryParams({limit: 10000, since: nextBatch}); - const {body, status} = await this._request(`${this.baseURL}/_matrix/client/r0/publicRooms?${queryParams}`).response(); - nextBatch = body.next_batch; - const publicRoom = body.chunk.find(c => c.room_id === roomId); - if (publicRoom) { - return publicRoom; - } - } while (nextBatch); - } + // MSC3266 implementation + async getRoomSummary(roomIdOrAlias, viaServers) { + let query; + if (viaServers.length > 0) { + query = "?" + viaServers.map(server => `via=${encodeURIComponent(server)}`).join('&'); + } + const {body, status} = await this._request(`${this.baseURL}/_matrix/client/unstable/im.nheko.summary/rooms/${encodeURIComponent(roomIdOrAlias)}/summary${query}`).response(); + if (status !== 200) return; + return body; + } + + async findPublicRoomById(roomId) { + const {body, status} = await this._request(`${this.baseURL}/_matrix/client/r0/directory/list/room/${encodeURIComponent(roomId)}`).response(); + if (status !== 200 || body.visibility !== "public") { + return; + } + let nextBatch; + do { + const queryParams = encodeQueryParams({limit: 10000, since: nextBatch}); + const {body, status} = await this._request(`${this.baseURL}/_matrix/client/r0/publicRooms?${queryParams}`).response(); + nextBatch = body.next_batch; + const publicRoom = body.chunk.find(c => c.room_id === roomId); + if (publicRoom) { + return publicRoom; + } + } while (nextBatch); + } - async getRoomIdFromAlias(alias) { - const {status, body} = await this._request(`${this.baseURL}/_matrix/client/r0/directory/room/${encodeURIComponent(alias)}`).response(); - if (status === 200) { - return body.room_id; - } - } + async getRoomIdFromAlias(alias) { + const {status, body} = await this._request(`${this.baseURL}/_matrix/client/r0/directory/room/${encodeURIComponent(alias)}`).response(); + if (status === 200) { + return body.room_id; + } + } async getPrivacyPolicyUrl(lang = "en") { const headers = new Map(); @@ -94,7 +109,7 @@ export class HomeServer { } } - mxcUrlThumbnail(url, width, height, method) { + mxcUrlThumbnail(url, width, height, method) { const parts = parseMxcUrl(url); if (parts) { const [serverName, mediaId] = parts; diff --git a/src/preview/PreviewView.js b/src/preview/PreviewView.js index 0fedfb6df1b0fdb80f3745f3aed69a1deadc8b04..8a06f8f9dbb6b2607db54e55d8703cd6dba9d7b3 100644 --- a/src/preview/PreviewView.js +++ b/src/preview/PreviewView.js @@ -43,7 +43,7 @@ class LoadingPreviewView extends TemplateView { } class LoadedPreviewView extends TemplateView { - render(t, vm) { + render(t, vm) { const avatar = t.map(vm => vm.avatarUrl, (avatarUrl, t) => { if (avatarUrl) { return t.img({className: "avatar", src: avatarUrl}); @@ -51,12 +51,12 @@ class LoadedPreviewView extends TemplateView { return t.div({className: "defaultAvatar"}); } }); - return t.div([ - t.div({className: "avatarContainer"}, avatar), - t.h1(vm => vm.name), - t.p({className: {identifier: true, hidden: vm => !vm.identifier}}, vm => vm.identifier), - t.div({className: {memberCount: true, hidden: vm => !vm.memberCount}}, t.p([vm => vm.memberCount, " members"])), - t.p({className: {topic: true, hidden: vm => !vm.topic}}, [vm => vm.topic]), - ]); - } + return t.div({className: vm.isSpaceRoom ? "mxSpace" : undefined}, [ + t.div({className: "avatarContainer"}, avatar), + t.h1(vm => vm.name), + t.p({className: {identifier: true, hidden: vm => !vm.identifier}}, vm => vm.identifier), + t.div({className: {memberCount: true, hidden: vm => !vm.memberCount}}, t.p([vm => vm.memberCount, " members"])), + t.p({className: {topic: true, hidden: vm => !vm.topic}}, [vm => vm.topic]), + ]); + } } diff --git a/src/preview/PreviewViewModel.js b/src/preview/PreviewViewModel.js index 50843e64dd57cb4ae091d1239d8b3dd1d16f30cd..e8c5776781e3cb19527ccd276287cbed3876b992 100644 --- a/src/preview/PreviewViewModel.js +++ b/src/preview/PreviewViewModel.js @@ -21,92 +21,101 @@ import {ClientListViewModel} from "../open/ClientListViewModel.js"; import {ClientViewModel} from "../open/ClientViewModel.js"; export class PreviewViewModel extends ViewModel { - constructor(options) { - super(options); - const { link, consentedServers } = options; - this._link = link; - this._consentedServers = consentedServers; - this.loading = false; - this.name = this._link.identifier; - this.avatarUrl = null; - this.identifier = null; - this.memberCount = null; - this.topic = null; - this.domain = null; + constructor(options) { + super(options); + const { link, consentedServers } = options; + this._link = link; + this._consentedServers = consentedServers; + this.loading = false; + this.name = this._link.identifier; + this.avatarUrl = null; + this.identifier = null; + this.memberCount = null; + this.topic = null; + this.domain = null; this.failed = false; - } + this.isSpaceRoom = false; + } - async load() { + async load() { const {kind} = this._link; const supportsPreview = kind === LinkKind.User || kind === LinkKind.Room || kind === LinkKind.Event; if (supportsPreview) { - this.loading = true; - this.emitChange(); - for (const server of this._consentedServers) { - try { - const homeserver = await resolveServer(this.request, server); - switch (this._link.kind) { - case LinkKind.User: - await this._loadUserPreview(homeserver, this._link.identifier); - break; - case LinkKind.Room: + this.loading = true; + this.emitChange(); + for (const server of this._consentedServers) { + try { + const homeserver = await resolveServer(this.request, server); + switch (this._link.kind) { + case LinkKind.User: + await this._loadUserPreview(homeserver, this._link.identifier); + break; + case LinkKind.Room: case LinkKind.Event: - await this._loadRoomPreview(homeserver, this._link); - break; - } - // assume we're done if nothing threw - this.domain = server; + await this._loadRoomPreview(homeserver, this._link); + break; + } + // assume we're done if nothing threw + this.domain = server; this.loading = false; - this.emitChange(); + this.emitChange(); return; - } catch (err) { - continue; - } - } + } catch (err) { + continue; + } + } } this.loading = false; - this._setNoPreview(this._link); + this._setNoPreview(this._link); if (this._consentedServers.length && supportsPreview) { this.domain = this._consentedServers[this._consentedServers.length - 1]; this.failed = true; } this.emitChange(); - } + } get hasTopic() { return this._link.kind === LinkKind.Room; } get hasMemberCount() { return this.hasTopic; } - async _loadUserPreview(homeserver, userId) { - const profile = await homeserver.getUserProfile(userId); - this.name = profile.displayname || userId; - this.avatarUrl = profile.avatar_url ? - homeserver.mxcUrlThumbnail(profile.avatar_url, 64, 64, "crop") : - null; - this.identifier = userId; - } + async _loadUserPreview(homeserver, userId) { + const profile = await homeserver.getUserProfile(userId); + this.name = profile.displayname || userId; + this.avatarUrl = profile.avatar_url ? + homeserver.mxcUrlThumbnail(profile.avatar_url, 64, 64, "crop") : + null; + this.identifier = userId; + } - async _loadRoomPreview(homeserver, link) { - let publicRoom; - if (link.identifierKind === IdentifierKind.RoomId) { - publicRoom = await homeserver.findPublicRoomById(link.identifier); - } else if (link.identifierKind === IdentifierKind.RoomAlias) { - const roomId = await homeserver.getRoomIdFromAlias(link.identifier); - if (roomId) { - publicRoom = await homeserver.findPublicRoomById(roomId); - } - } - this.name = publicRoom?.name || publicRoom?.canonical_alias || link.identifier; - this.avatarUrl = publicRoom?.avatar_url ? - homeserver.mxcUrlThumbnail(publicRoom.avatar_url, 64, 64, "crop") : - null; - this.memberCount = publicRoom?.num_joined_members; - this.topic = publicRoom?.topic; - this.identifier = publicRoom?.canonical_alias || link.identifier; + async _loadRoomPreview(homeserver, link) { + let publicRoom; + if (link.identifierKind === IdentifierKind.RoomId || link.identifierKind === IdentifierKind.RoomAlias) { + publicRoom = await homeserver.getRoomSummary(link.identifier, link.servers); + } + + if (!publicRoom) { + if (link.identifierKind === IdentifierKind.RoomId) { + publicRoom = await homeserver.findPublicRoomById(link.identifier); + } else if (link.identifierKind === IdentifierKind.RoomAlias) { + const roomId = await homeserver.getRoomIdFromAlias(link.identifier); + if (roomId) { + publicRoom = await homeserver.findPublicRoomById(roomId); + } + } + } + + this.name = publicRoom?.name || publicRoom?.canonical_alias || link.identifier; + this.avatarUrl = publicRoom?.avatar_url ? + homeserver.mxcUrlThumbnail(publicRoom.avatar_url, 64, 64, "crop") : + null; + this.memberCount = publicRoom?.num_joined_members; + this.topic = publicRoom?.topic; + this.identifier = publicRoom?.canonical_alias || link.identifier; + this.isSpaceRoom = publicRoom?.room_type === "m.space"; if (this.identifier === this.name) { this.identifier = null; } - } + } _setNoPreview(link) { this.name = link.identifier;