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;