diff --git a/package.json b/package.json
index 586b537e87ebd32f95a13816824cbc974a6db78f..28b1609df84a81127e2d3320ad550a9a89c2058b 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,6 @@
         "@quentin-sommer/react-useragent": "^3.1.0",
         "classnames": "^2.2.6",
         "formik": "^2.1.4",
-        "matrix-cypher": "^0.1.12",
         "react": "^16.13.1",
         "react-dom": "^16.13.1",
         "react-scripts": "3.4.1",
diff --git a/src/App.scss b/src/App.scss
index 0f0855ec36711353df935103d0260a2279718bbb..7aff62a9a6bf5565a76eb11400729b1fae6d5947 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -20,6 +20,7 @@ limitations under the License.
     background-color: $app-background;
     background-image: url('./imgs/background.svg');
     background-repeat: no-repeat;
+    background-size: stretch;
     background-position: 50% -20%;
 }
 
@@ -32,7 +33,7 @@ limitations under the License.
 .topSpacer {
     @include spacer;
 
-    height: 20vh;
+    height: 10vh;
 }
 
 .bottomSpacer {
diff --git a/src/App.tsx b/src/App.tsx
index 5f604562264db779d404eb2ff44208a3862d5cc7..376f7b63c15c8b90250e208d1cae3485de8dd642 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
+import React, { useState, useEffect } from 'react';
 
 import SingleColumn from './layouts/SingleColumn';
 import CreateLinkTile from './components/CreateLinkTile';
 import MatrixTile from './components/MatrixTile';
 import Tile from './components/Tile';
 import LinkRouter from './pages/LinkRouter';
+import Footer from './components/Footer';
 
 import './App.scss';
 
@@ -32,13 +33,25 @@ const App: React.FC = () => {
     let page = (
         <>
             <CreateLinkTile />
-            <hr />
         </>
     );
 
-    if (location.hash) {
-        if (location.hash.startsWith('#/')) {
-            page = <LinkRouter link={location.hash.slice(2)} />;
+    const [hash, setHash] = useState(location.hash);
+
+    console.log(hash);
+    useEffect(() => {
+        // Some hacky uri decoding
+        if (location.href.split('/').length > 4) {
+            location.href = decodeURIComponent(location.href);
+        }
+
+        window.onhashchange = () => setHash(location.hash);
+        console.log('why');
+    }, []);
+
+    if (hash) {
+        if (hash.startsWith('#/')) {
+            page = <LinkRouter link={hash.slice(2)} />;
         } else {
             page = (
                 <Tile>
@@ -50,12 +63,18 @@ const App: React.FC = () => {
     }
 
     return (
-        <SingleColumn>
-            <div className="topSpacer" />
-            <GlobalContext>{page}</GlobalContext>
-            <MatrixTile />
-            <div className="bottomSpacer" />
-        </SingleColumn>
+        <GlobalContext>
+            <SingleColumn>
+                <div className="topSpacer" />
+                {page}
+                <div>
+                    <MatrixTile isLink={!!location.hash} />
+                    <br />
+                    <Footer />
+                </div>
+                <div className="bottomSpacer" />
+            </SingleColumn>
+        </GlobalContext>
     );
 };
 
diff --git a/src/clients/Element.io.ts b/src/clients/Element.io.ts
index 6d4f6c13024a9b8fa2c74fc65b0a1a645a7c8e52..8a346ae8995ec965f8a7a069df4aa4160acac6f6 100644
--- a/src/clients/Element.io.ts
+++ b/src/clients/Element.io.ts
@@ -56,6 +56,7 @@ const Element: LinkedClient = {
                 );
         }
     },
+    linkSupport: () => true,
 };
 
 export const ElementDevelop: LinkedClient = {
@@ -90,5 +91,6 @@ export const ElementDevelop: LinkedClient = {
                 );
         }
     },
+    linkSupport: () => true,
 };
 export default Element;
diff --git a/src/clients/Fractal.tsx b/src/clients/Fractal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a0caaacf6e3530575428d2894cf5c8b4c4f0a101
--- /dev/null
+++ b/src/clients/Fractal.tsx
@@ -0,0 +1,69 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+
+import { TextClient, Maturity, ClientKind, ClientId, Platform } from './types';
+
+import { LinkKind } from '../parser/types';
+
+import logo from '../imgs/fractal.png';
+
+const Fractal: TextClient = {
+    kind: ClientKind.TEXT_CLIENT,
+    name: 'Fractal',
+    logo: logo,
+    author: 'Daniel Garcia Moreno',
+    homepage: 'https://github.com/poljar/weechat-matrix',
+    maturity: Maturity.BETA,
+    experimental: false,
+    platform: Platform.Desktop,
+    clientId: ClientId.Fractal,
+    toInviteString: (link) => {
+        switch (link.kind) {
+            case LinkKind.Alias:
+            case LinkKind.RoomId:
+            case LinkKind.UserId:
+                return <span>Click the '+' button in the top right</span>;
+            default:
+                return <span>Weechat doesn't support this kind of link</span>;
+        }
+    },
+    copyString: (link) => {
+        switch (link.kind) {
+            case LinkKind.Alias:
+            case LinkKind.RoomId:
+            case LinkKind.UserId:
+                return `${link.identifier}`;
+            default:
+                return '';
+        }
+    },
+    linkSupport: (link) => {
+        switch (link.kind) {
+            case LinkKind.Alias:
+            case LinkKind.RoomId:
+            case LinkKind.UserId:
+                return true;
+            default:
+                return false;
+        }
+    },
+
+    description: 'Command-line Matrix interface using Weechat',
+};
+
+export default Fractal;
diff --git a/src/clients/Nheko.tsx b/src/clients/Nheko.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a0954d1ca3ace2e66270d3400eef6e42eb405e48
--- /dev/null
+++ b/src/clients/Nheko.tsx
@@ -0,0 +1,85 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+
+import { TextClient, Maturity, ClientKind, ClientId, Platform } from './types';
+
+import { LinkKind } from '../parser/types';
+
+import logo from '../imgs/nheko.svg';
+
+const Nheko: TextClient = {
+    kind: ClientKind.TEXT_CLIENT,
+    name: 'Nheko',
+    logo: logo,
+    author: 'mujx, red_sky, deepbluev7, Konstantinos Sideris',
+    homepage: 'https://github.com/Nheko-Reborn/nheko',
+    maturity: Maturity.BETA,
+    experimental: false,
+    platform: Platform.Desktop,
+    clientId: ClientId.Nheko,
+    toInviteString: (link) => {
+        switch (link.kind) {
+            case LinkKind.Alias:
+            case LinkKind.RoomId:
+                return (
+                    <span>
+                        Type{' '}
+                        <code>
+                            /join <b>{link.identifier}</b>
+                        </code>
+                    </span>
+                );
+            case LinkKind.UserId:
+                return (
+                    <span>
+                        Type{' '}
+                        <code>
+                            /invite <b>{link.identifier}</b>
+                        </code>
+                    </span>
+                );
+            default:
+                return <span>Nheko doesn't support this kind of link</span>;
+        }
+    },
+    copyString: (link) => {
+        switch (link.kind) {
+            case LinkKind.Alias:
+            case LinkKind.RoomId:
+                return `/join ${link.identifier}`;
+            case LinkKind.UserId:
+                return `/invite ${link.identifier}`;
+            default:
+                return '';
+        }
+    },
+    linkSupport: (link) => {
+        switch (link.kind) {
+            case LinkKind.Alias:
+            case LinkKind.RoomId:
+            case LinkKind.UserId:
+                return true;
+            default:
+                return false;
+        }
+    },
+    description:
+        'A native desktop app for Matrix that feels more like a mainstream chat app.',
+};
+
+export default Nheko;
diff --git a/src/clients/Weechat.tsx b/src/clients/Weechat.tsx
index ff6b52899813b4b0654bac429f15c22574d6a08e..cd15769017bc2b90840ee07f1d87845759b57fa4 100644
--- a/src/clients/Weechat.tsx
+++ b/src/clients/Weechat.tsx
@@ -68,6 +68,17 @@ const Weechat: TextClient = {
                 return '';
         }
     },
+    linkSupport: (link) => {
+        switch (link.kind) {
+            case LinkKind.Alias:
+            case LinkKind.RoomId:
+            case LinkKind.UserId:
+                return true;
+            default:
+                return false;
+        }
+    },
+
     description: 'Command-line Matrix interface using Weechat',
 };
 
diff --git a/src/clients/index.ts b/src/clients/index.ts
index ba39676ddeaff27fcc6bc50ab631723c00abab9c..c3686bb3e41b0f1c0d5d2209bb2bbbd3e9ee953a 100644
--- a/src/clients/index.ts
+++ b/src/clients/index.ts
@@ -18,11 +18,13 @@ import { Client } from './types';
 
 import Element, { ElementDevelop } from './Element.io';
 import Weechat from './Weechat';
+import Nheko from './Nheko';
+import Fractal from './Fractal';
 
 /*
  * All the supported clients of matrix.to
  */
-const clients: Client[] = [Element, Weechat, ElementDevelop];
+const clients: Client[] = [Element, Weechat, Nheko, Fractal, ElementDevelop];
 
 /*
  * A map from sharer string to client.
@@ -33,6 +35,8 @@ export const clientMap: { [key: string]: Client } = {
     [Element.clientId]: Element,
     [Weechat.clientId]: Weechat,
     [ElementDevelop.clientId]: ElementDevelop,
+    [Nheko.clientId]: Nheko,
+    [Fractal.clientId]: Fractal,
 };
 
 /*
diff --git a/src/clients/types.ts b/src/clients/types.ts
index cdaed2785dd5ce19106ca277e3ff99bf940fa9e7..5a93f15848582ff6e2287900f47ab747c1142599 100644
--- a/src/clients/types.ts
+++ b/src/clients/types.ts
@@ -49,6 +49,8 @@ export enum ClientId {
     Element = 'element.io',
     ElementDevelop = 'develop.element.io',
     WeeChat = 'weechat',
+    Nheko = 'nheko',
+    Fractal = 'fractal',
 }
 
 /*
@@ -64,6 +66,7 @@ export interface ClientDescription {
     maturity: Maturity;
     clientId: ClientId;
     experimental: boolean;
+    linkSupport: (link: SafeLink) => boolean;
 }
 
 /*
diff --git a/src/components/Avatar.scss b/src/components/Avatar.scss
index 7895a82fa33a2391b12f2c66aa896fc96c7a87b6..794064aa6030e767cca36210e44872493c2b9fbb 100644
--- a/src/components/Avatar.scss
+++ b/src/components/Avatar.scss
@@ -14,11 +14,15 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-@import "../color-scheme";
+@import '../color-scheme';
 
 .avatar {
     border-radius: 100%;
     border: 1px solid $borders;
-    height: 50px;
-    width: 50px;
+    height: 60px;
+    width: 60px;
+}
+
+.avatarNoCrop {
+    border-radius: 0;
 }
diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx
index 003d004dc1db62992fbdae68b2d8da73a0ade525..f150c6333e1d2998919cf8f3e58ad5fb3230c902 100644
--- a/src/components/Avatar.tsx
+++ b/src/components/Avatar.tsx
@@ -16,10 +16,10 @@ limitations under the License.
 
 import React, { useEffect, useState } from 'react';
 import classNames from 'classnames';
-import { Room, User } from 'matrix-cypher';
+import { Room, User } from '../matrix-cypher';
 
 import { getMediaQueryFromMCX } from '../utils/cypher-wrapper';
-import logo from '../imgs/matrix-logo.svg';
+import logo from '../imgs/chat-icon.svg';
 
 import './Avatar.scss';
 
diff --git a/src/components/Button.scss b/src/components/Button.scss
index c34c52124b38c19891a11d001b1cf44da61805cb..c1a6d70dfbcd8fefc2cd804fa12150eaaf484fac 100644
--- a/src/components/Button.scss
+++ b/src/components/Button.scss
@@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-@import "../color-scheme";
+@import '../color-scheme';
 
 .button {
     width: 100%;
 
-    padding: 1rem;
+    height: 48px;
+
     border-radius: 2rem;
     border: 0;
 
@@ -28,6 +29,31 @@ limitations under the License.
 
     font-size: 14px;
     font-weight: 500;
+
+    &:hover {
+        cursor: pointer;
+    }
+
+    position: relative;
+
+    .buttonIcon {
+        position: absolute;
+        height: 24px;
+        width: 24px;
+
+        left: 18px;
+        top: 12px;
+    }
+}
+
+.buttonSecondary {
+    background-color: $background;
+    color: $foreground;
+    border: 1px solid $foreground;
+}
+
+.errorButton:hover {
+    cursor: not-allowed;
 }
 
 .buttonHighlight {
diff --git a/src/components/Button.stories.tsx b/src/components/Button.stories.tsx
index 1f5d2e591e35df3ec8e0650e0804d8e922a098d3..25cfee83962d2030a82e09852d38a0792b9c7b6d 100644
--- a/src/components/Button.stories.tsx
+++ b/src/components/Button.stories.tsx
@@ -27,3 +27,7 @@ export const WithText: React.FC = () => (
         {text('label', 'Hello Story Book')}
     </Button>
 );
+
+export const Secondary: React.FC = () => (
+    <Button secondary>Secondary button</Button>
+);
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index a347254ccb1791316ca012cc91efb53efb0ea53d..fd031f2b166b89a84e0c28c2da37db1922041d7a 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -22,6 +22,9 @@ import './Button.scss';
 interface IProps extends React.ButtonHTMLAttributes<Element> {
     // Briefly display these instead of the children onClick
     flashChildren?: React.ReactNode;
+    secondary?: boolean;
+    icon?: string;
+    flashIcon?: string;
 }
 
 /**
@@ -31,7 +34,16 @@ const Button: React.FC<
     IProps & React.RefAttributes<HTMLButtonElement>
 > = React.forwardRef(
     (
-        { onClick, children, flashChildren, className, ...props }: IProps,
+        {
+            onClick,
+            children,
+            flashChildren,
+            className,
+            secondary,
+            icon,
+            flashIcon,
+            ...props
+        }: IProps,
         ref: React.Ref<HTMLButtonElement>
     ) => {
         const [wasClicked, setWasClicked] = React.useState(false);
@@ -51,8 +63,15 @@ const Button: React.FC<
 
         const classNames = classnames('button', className, {
             buttonHighlight: wasClicked,
+            buttonSecondary: secondary,
         });
 
+        const iconSrc = wasClicked && flashIcon ? flashIcon : icon;
+
+        const buttonIcon = icon ? (
+            <img className="buttonIcon" src={iconSrc} alt="" />
+        ) : null;
+
         return (
             <button
                 className={classNames}
@@ -60,6 +79,7 @@ const Button: React.FC<
                 ref={ref}
                 {...props}
             >
+                {buttonIcon}
                 {content}
             </button>
         );
diff --git a/src/components/ClientList.tsx b/src/components/ClientList.tsx
index fbef28fc625643adb012c0b96af566d3c5f7400f..4d23d37387e97c59fc3af24ba914cac383ee3d3f 100644
--- a/src/components/ClientList.tsx
+++ b/src/components/ClientList.tsx
@@ -63,6 +63,10 @@ const ClientList: React.FC<IProps> = ({ link, rememberSelection }: IProps) => {
             showClient = false;
         }
 
+        if (!client.linkSupport(link)) {
+            showClient = false;
+        }
+
         return showClient;
     };
 
diff --git a/src/components/ClientSelection.tsx b/src/components/ClientSelection.tsx
index bb5864231bcc710e9c76b0595afc3426c3cb0c0d..9989b295aac5b80c03366237cc8cb07c6c1e445e 100644
--- a/src/components/ClientSelection.tsx
+++ b/src/components/ClientSelection.tsx
@@ -38,7 +38,7 @@ const ClientSelection: React.FC<IProps> = ({ link }: IProps) => {
                 }}
                 checked={rememberSelection}
             >
-                Remember my selection for future invites in this browser
+                Remember choice for future invites in this browser
             </StyledCheckbox>
             <StyledCheckbox
                 onChange={(): void => {
@@ -79,7 +79,6 @@ const ClientSelection: React.FC<IProps> = ({ link }: IProps) => {
     return (
         <div className="advanced">
             {options}
-            <h4>Clients you can accept this invite with</h4>
             <ClientList link={link} rememberSelection={rememberSelection} />
             {clearSelection}
         </div>
diff --git a/src/components/ClientTile.scss b/src/components/ClientTile.scss
index 0b8f0916f9cc33988d8ad87158669602c04f7411..996c56f672ec87db12321c88d3cbac3f6334b40f 100644
--- a/src/components/ClientTile.scss
+++ b/src/components/ClientTile.scss
@@ -19,7 +19,7 @@ limitations under the License.
 .clientTile {
     display: flex;
     flex-direction: row;
-    align-items: center;
+    align-items: flex-start;
 
     min-height: 150px;
     width: 100%;
@@ -28,7 +28,10 @@ limitations under the License.
 
     > img {
         flex-shrink: 0;
-        height: 130px;
+        height: 116px;
+        width: 116px;
+        margin-right: 14px;
+        border-radius: 16px;
     }
 
     > div {
@@ -42,16 +45,17 @@ limitations under the License.
         }
 
         p {
-            margin-right: 20px;
+            margin-right: 8px;
             text-align: left;
         }
 
         .button {
-            margin: 5px;
+            height: 40px;
+            width: 130px;
+            margin-top: 16px;
         }
     }
 
-    border: 1px solid $borders;
     border-radius: 8px;
 
     padding: 15px;
@@ -59,8 +63,8 @@ limitations under the License.
     // For the chevron
     position: relative;
 
-    &::hover {
-        background-color: $grey;
+    &:hover {
+        background-color: $app-background;
     }
 }
 
@@ -68,12 +72,4 @@ limitations under the License.
     position: relative;
 
     width: 100%;
-
-    &::after {
-        // TODO: add chevron top right
-        position: absolute;
-        right: 10px;
-        top: 5px;
-        content: '>';
-    }
 }
diff --git a/src/components/CreateLinkTile.scss b/src/components/CreateLinkTile.scss
index ada385663739988934b3792c6af16843a66eaf19..5a300ce283edd66e386ccfac98ea008c6e16d094 100644
--- a/src/components/CreateLinkTile.scss
+++ b/src/components/CreateLinkTile.scss
@@ -29,7 +29,6 @@ limitations under the License.
         display: grid;
         row-gap: 24px;
         align-self: center;
-        padding: 0 30px;
     }
 
     > a {
@@ -39,4 +38,56 @@ limitations under the License.
     h1 {
         word-break: break-all;
     }
+
+    .createLinkReset {
+        height: 40px;
+        width: 40px;
+
+        border-radius: 100%;
+        border: 1px solid lighten($grey, 50%);
+
+        background: $background;
+
+        padding: 6px;
+
+        position: relative;
+
+        > div {
+            // This is a terrible case of faking it till
+            // we make it. It will break. I'm so sorry
+            position: absolute;
+            display: none;
+
+            width: max-content;
+            top: -35px;
+            left: -17px;
+
+            border-radius: 30px;
+            padding: 5px 15px;
+
+            background: $background;
+
+            word-wrap: none;
+        }
+
+        img {
+            height: 100%;
+            width: 100%;
+            border: 0;
+
+            filter: invert(12%);
+        }
+
+        &:hover {
+            border: 0;
+
+            background: $foreground;
+
+            cursor: pointer;
+
+            > div {
+                display: block;
+            }
+        }
+    }
 }
diff --git a/src/components/CreateLinkTile.tsx b/src/components/CreateLinkTile.tsx
index 3553289ac47f5bf9719b1b9d4936034272405ea3..12ffd5485bea42577b5616d229f593de328a0b55 100644
--- a/src/components/CreateLinkTile.tsx
+++ b/src/components/CreateLinkTile.tsx
@@ -19,11 +19,13 @@ import { Formik, Form } from 'formik';
 
 import Tile from './Tile';
 import Button from './Button';
-import TextButton from './TextButton';
 import Input from './Input';
 import { parseHash } from '../parser/parser';
 import { LinkKind } from '../parser/types';
-
+import linkIcon from '../imgs/link.svg';
+import copyIcon from '../imgs/copy.svg';
+import tickIcon from '../imgs/tick.svg';
+import refreshIcon from '../imgs/refresh.svg';
 import './CreateLinkTile.scss';
 
 interface ILinkNotCreatedTileProps {
@@ -38,11 +40,16 @@ interface FormValues {
 function validate(values: FormValues): Partial<FormValues> {
     const errors: Partial<FormValues> = {};
 
+    if (values.identifier === '') {
+        errors.identifier = '';
+        return errors;
+    }
+
     const parse = parseHash(values.identifier);
 
     if (parse.kind === LinkKind.ParseFailed) {
         errors.identifier =
-            "That link doesn't look right. Double check the details.";
+            "That identifier doesn't look right. Double check the details.";
     }
 
     return errors;
@@ -71,15 +78,28 @@ const LinkNotCreatedTile: React.FC<ILinkNotCreatedTileProps> = (
                             values.identifier
                     );
                 }}
+                validateOnChange={false}
             >
-                <Form>
-                    <Input
-                        name={'identifier'}
-                        type={'text'}
-                        placeholder="#room:example.com, @user:example.com"
-                    />
-                    <Button type="submit">Get Link</Button>
-                </Form>
+                {(formik): JSX.Element => (
+                    <Form>
+                        <Input
+                            name={'identifier'}
+                            type={'text'}
+                            placeholder="#room:example.com, @user:example.com"
+                            autoFocus
+                        />
+                        <Button
+                            type="submit"
+                            icon={linkIcon}
+                            disabled={!!formik.errors.identifier}
+                            className={
+                                formik.errors.identifier ? 'errorButton' : ''
+                            }
+                        >
+                            Create Link
+                        </Button>
+                    </Form>
+                )}
             </Formik>
         </Tile>
     );
@@ -102,14 +122,20 @@ const LinkCreatedTile: React.FC<ILinkCreatedTileProps> = (props) => {
 
     return (
         <Tile className="createLinkTile">
-            <TextButton onClick={(): void => props.setLink('')}>
-                Create another link
-            </TextButton>
+            <button
+                className="createLinkReset"
+                onClick={(): void => props.setLink('')}
+            >
+                <div>New link</div>
+                <img src={refreshIcon} />
+            </button>
             <a href={props.link}>
                 <h1>{props.link}</h1>
             </a>
             <Button
                 flashChildren={'Copied'}
+                icon={copyIcon}
+                flashIcon={tickIcon}
                 onClick={(): void => {
                     navigator.clipboard.writeText(props.link);
                 }}
diff --git a/src/components/DefaultPreview.scss b/src/components/DefaultPreview.scss
index 21b77bb06d28eabe6873f7ee96d5e92d527abfa9..48e404ca8b033a8f33b267e7eb9dfd1ffda30607 100644
--- a/src/components/DefaultPreview.scss
+++ b/src/components/DefaultPreview.scss
@@ -19,4 +19,8 @@ limitations under the License.
         border-radius: 0;
         border: 0;
     }
+
+    h1 {
+        word-break: break-all;
+    }
 }
diff --git a/src/components/EventPreview.tsx b/src/components/EventPreview.tsx
index de7ac7f019c63b10a3a7ea75bd761e56d829730f..00ac979efe0e6903cc40e1ab432b83115ebab953 100644
--- a/src/components/EventPreview.tsx
+++ b/src/components/EventPreview.tsx
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 import React from 'react';
-import { Room, Event } from 'matrix-cypher';
+import { Room, Event } from '../matrix-cypher';
 
 import RoomPreview from './RoomPreview';
 
diff --git a/src/components/Footer.scss b/src/components/Footer.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b416bbb4fc10785538eebaf1917ebfa189a7f6c2
--- /dev/null
+++ b/src/components/Footer.scss
@@ -0,0 +1,33 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+@import '../color-scheme';
+
+.footer {
+    display: grid;
+    grid-auto-flow: column;
+    justify-content: center;
+    column-gap: 5px;
+
+    * {
+        color: $font;
+    }
+
+    .textButton {
+        margin: 0;
+        padding: 0;
+    }
+}
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e8578e5cad4f80abfd64b2247e9d5f771e4c5a54
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,65 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useContext } from 'react';
+
+import HSContext, {
+    HSOptions,
+    ActionType as HSACtionType,
+} from '../contexts/HSContext';
+import ClientContext, {
+    ActionType as ClientActionType,
+} from '../contexts/ClientContext';
+import TextButton from './TextButton';
+
+import './Footer.scss';
+
+const Footer: React.FC = () => {
+    const [hsState, hsDispatch] = useContext(HSContext);
+    const [clientState, clientDispatch] = useContext(ClientContext);
+
+    const clear =
+        hsState.option !== HSOptions.Unset || clientState.clientId !== null ? (
+            <>
+                {' · '}
+                <TextButton
+                    onClick={(): void => {
+                        hsDispatch({
+                            action: HSACtionType.Clear,
+                        });
+                        clientDispatch({
+                            action: ClientActionType.ClearClient,
+                        });
+                    }}
+                >
+                    Clear preferences
+                </TextButton>
+            </>
+        ) : null;
+
+    return (
+        <div className="footer">
+            <a href="https://github.com/matrix-org/matrix.to">GitHub</a>
+            {' · '}
+            <a href="https://github.com/matrix-org/matrix.to/tree/matrix-two/src/clients">
+                Add your client
+            </a>
+            {clear}
+        </div>
+    );
+};
+
+export default Footer;
diff --git a/src/components/HomeserverOptions.scss b/src/components/HomeserverOptions.scss
index b58f372bbdde91bc17572de89d994e490d064796..f0736c0da8b628244f6610c6c1dbd5acb0756ca6 100644
--- a/src/components/HomeserverOptions.scss
+++ b/src/components/HomeserverOptions.scss
@@ -46,6 +46,7 @@ limitations under the License.
             width: 62px;
             padding: 11px;
             border-radius: 100%;
+            margin-left: 14px;
         }
     }
 
diff --git a/src/components/HomeserverOptions.stories.tsx b/src/components/HomeserverOptions.stories.tsx
index e39563521c9b47b9dfdfcdf38bb6113a00288c77..8a75042aa53eff2c33ee48c875f00bcad5e7d266 100644
--- a/src/components/HomeserverOptions.stories.tsx
+++ b/src/components/HomeserverOptions.stories.tsx
@@ -17,6 +17,7 @@ limitations under the License.
 import React from 'react';
 
 import HomeserverOptions from './HomeserverOptions';
+import { LinkKind } from '../parser/types';
 
 export default {
     title: 'HomeserverOptions',
@@ -29,4 +30,13 @@ export default {
     },
 };
 
-export const Default: React.FC = () => <HomeserverOptions />;
+export const Default: React.FC = () => (
+    <HomeserverOptions
+        link={{
+            identifier: '#banter:matrix.org',
+            arguments: { vias: [] },
+            kind: LinkKind.Alias,
+            originalLink: 'This is all made up',
+        }}
+    />
+);
diff --git a/src/components/HomeserverOptions.tsx b/src/components/HomeserverOptions.tsx
index de3d51ab03a15d6d59b5448affa9f73566db47dd..ab30a3c724d37c7c44e239da3c7872785786dceb 100644
--- a/src/components/HomeserverOptions.tsx
+++ b/src/components/HomeserverOptions.tsx
@@ -23,12 +23,14 @@ import HSContext, { TempHSContext, ActionType } from '../contexts/HSContext';
 import icon from '../imgs/telecom-mast.svg';
 import Button from './Button';
 import Input from './Input';
-import Toggle from './Toggle';
 import StyledCheckbox from './StyledCheckbox';
+import { SafeLink } from '../parser/types';
 
 import './HomeserverOptions.scss';
 
-interface IProps {}
+interface IProps {
+    link: SafeLink;
+}
 
 interface FormValues {
     HSUrl: string;
@@ -39,21 +41,25 @@ function validateURL(values: FormValues): Partial<FormValues> {
     try {
         string().url().parse(values.HSUrl);
     } catch {
-        errors.HSUrl = 'This must be a valid url';
+        errors.HSUrl =
+            'This must be a valid homeserver URL, starting with https://';
     }
     return errors;
 }
 
-const HomeserverOptions: React.FC<IProps> = () => {
+const HomeserverOptions: React.FC<IProps> = ({ link }: IProps) => {
     const HSStateDispatcher = useContext(HSContext)[1];
     const TempHSStateDispatcher = useContext(TempHSContext)[1];
+
     const [rememberSelection, setRemeberSelection] = useState(false);
-    const [usePrefered, setUsePrefered] = useState(false);
+
+    // Select which disaptcher to use based on whether we're writing
+    // the choice to localstorage
     const dispatcher = rememberSelection
         ? HSStateDispatcher
         : TempHSStateDispatcher;
 
-    const hsInput = usePrefered ? (
+    const hsInput = (
         <Formik
             initialValues={{
                 HSUrl: '',
@@ -63,23 +69,33 @@ const HomeserverOptions: React.FC<IProps> = () => {
                 dispatcher({ action: ActionType.SetHS, HSURL: HSUrl })
             }
         >
-            <Form>
-                <Input
-                    type="text"
-                    name="HSUrl"
-                    placeholder="https://example.com"
-                />
-                <Button type="submit">Set HS</Button>
-            </Form>
+            {({ values, errors }): JSX.Element => (
+                <Form>
+                    <Input
+                        muted={!values.HSUrl}
+                        type="text"
+                        name="HSUrl"
+                        placeholder="Preferred homeserver URL"
+                    />
+                    {values.HSUrl && !errors.HSUrl ? (
+                        <Button secondary type="submit">
+                            Use {values.HSUrl}
+                        </Button>
+                    ) : null}
+                </Form>
+            )}
         </Formik>
-    ) : null;
+    );
 
     return (
         <Tile className="homeserverOptions">
             <div className="homeserverOptionsDescription">
                 <div>
+                    <h3>About {link.identifier}</h3>
                     <p>
-                        Let's locate a homeserver to show you more information.
+                        A homeserver will show you metadata about the link, like
+                        a description. Homeservers will be able to relate your
+                        IP to things you've opened invites for in matrix.to.
                     </p>
                 </div>
                 <img
@@ -94,18 +110,13 @@ const HomeserverOptions: React.FC<IProps> = () => {
                 Remember my choice
             </StyledCheckbox>
             <Button
+                secondary
                 onClick={(): void => {
                     dispatcher({ action: ActionType.SetAny });
                 }}
             >
                 Use any homeserver
             </Button>
-            <Toggle
-                checked={usePrefered}
-                onChange={(): void => setUsePrefered(!usePrefered)}
-            >
-                Use my preferred homeserver only
-            </Toggle>
             {hsInput}
         </Tile>
     );
diff --git a/src/components/Input.scss b/src/components/Input.scss
index e6ca73589f53b1401cf149266649517b9dfa7c3c..f052af11db2138221d69dc429ec50e47cbdf8f8b 100644
--- a/src/components/Input.scss
+++ b/src/components/Input.scss
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-@import "../color-scheme";
-@import "../error";
+@import '../color-scheme';
+@import '../error';
 
 .input {
     width: 100%;
@@ -23,7 +23,8 @@ limitations under the License.
 
     background: $background;
 
-    border: 1px solid $font;
+    border: 1px solid $foreground;
+    font: lighten($grey, 60%);
     border-radius: 24px;
 
     font-size: 14px;
@@ -32,9 +33,18 @@ limitations under the License.
     &.error {
         @include error;
     }
+
+    &:focus {
+        border: 1px solid $font;
+        font: $font;
+    }
 }
 
 .inputError {
     @include error;
     text-align: center;
 }
+
+.inputMuted {
+    border-color: lighten($grey, 60%);
+}
diff --git a/src/components/Input.tsx b/src/components/Input.tsx
index 7a50385cc278195a9ffed62ef034253e3b2f4149..21ce3424b0d140bc2898aee73f9f483ced2fedb4 100644
--- a/src/components/Input.tsx
+++ b/src/components/Input.tsx
@@ -20,21 +20,23 @@ import { useField } from 'formik';
 
 import './Input.scss';
 
-interface IProps extends React.InputHTMLAttributes<Element> {
+interface IProps extends React.InputHTMLAttributes<HTMLElement> {
     name: string;
     type: string;
+    muted?: boolean;
 }
 
-const Input: React.FC<IProps> = ({ className, ...props }) => {
+const Input: React.FC<IProps> = ({ className, muted, ...props }) => {
     const [field, meta] = useField(props);
 
-    const error =
-        meta.touched && meta.error ? (
-            <div className="inputError">{meta.error}</div>
-        ) : null;
+    const errorBool = meta.touched && meta.value !== '' && meta.error;
+    const error = errorBool ? (
+        <div className="inputError">{meta.error}</div>
+    ) : null;
 
     const classNames = classnames('input', className, {
-        error: meta.error,
+        error: errorBool,
+        inputMuted: !!muted,
     });
 
     return (
diff --git a/src/components/InviteTile.scss b/src/components/InviteTile.scss
index 042192719668df728970908d6fa01858072f8002..f9a8cdd854e035282c63f2c91436bf4030f4b3c9 100644
--- a/src/components/InviteTile.scss
+++ b/src/components/InviteTile.scss
@@ -14,15 +14,26 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+@import '../color-scheme';
+
 .inviteTile {
     display: grid;
     row-gap: 24px;
 
     .inviteTileClientSelection {
-        margin: 0 5%;
+        margin: 0 auto;
         display: grid;
 
         justify-content: space-between;
         row-gap: 20px;
+
+        h2 + p {
+            color: $foreground;
+        }
+    }
+
+    hr {
+        width: 100%;
+        margin: 0;
     }
 }
diff --git a/src/components/InviteTile.tsx b/src/components/InviteTile.tsx
index a35b2c7472140166d894796c8184b6fea2965fea..269e47edeac1427601fa78e1792f49b5e2137060 100644
--- a/src/components/InviteTile.tsx
+++ b/src/components/InviteTile.tsx
@@ -25,7 +25,6 @@ import ClientSelection from './ClientSelection';
 import { Client, ClientKind } from '../clients/types';
 import { SafeLink } from '../parser/types';
 import TextButton from './TextButton';
-import FakeProgress from './FakeProgress';
 
 interface IProps {
     children?: React.ReactNode;
@@ -39,10 +38,8 @@ const InviteTile: React.FC<IProps> = ({ children, client, link }: IProps) => {
     let advanced: React.ReactNode;
 
     if (client === null) {
-        invite = showAdvanced ? (
-            <FakeProgress />
-        ) : (
-            <Button onClick={() => setShowAdvanced(!showAdvanced)}>
+        invite = showAdvanced ? null : (
+            <Button onClick={(): void => setShowAdvanced(!showAdvanced)}>
                 Accept invite
             </Button>
         );
@@ -89,7 +86,9 @@ const InviteTile: React.FC<IProps> = ({ children, client, link }: IProps) => {
         if (client === null) {
             advanced = (
                 <>
-                    <h4>Pick an app to accept the invite with</h4>
+                    <hr />
+                    <h2>Almost done!</h2>
+                    <p>Great, pick a client below to confirm and continue</p>
                     <ClientSelection link={link} />
                 </>
             );
@@ -104,12 +103,15 @@ const InviteTile: React.FC<IProps> = ({ children, client, link }: IProps) => {
         }
     }
 
+    advanced = advanced ? (
+        <div className="inviteTileClientSelection">{advanced}</div>
+    ) : null;
     return (
         <>
             <Tile className="inviteTile">
                 {children}
                 {invite}
-                <div className="inviteTileClientSelection">{advanced}</div>
+                {advanced}
             </Tile>
         </>
     );
diff --git a/src/components/LinkPreview.tsx b/src/components/LinkPreview.tsx
index bcdb7be7d690d805210975405d287157e197a5b8..ae4170c8e4462624017c1d02e1b3aa3b624bf496 100644
--- a/src/components/LinkPreview.tsx
+++ b/src/components/LinkPreview.tsx
@@ -15,15 +15,16 @@ limitations under the License.
 */
 
 import React, { useState, useEffect, useContext } from 'react';
-import { getEvent, client } from 'matrix-cypher';
+import { getEvent, client } from '../matrix-cypher';
 
 import { RoomPreviewWithTopic } from './RoomPreview';
 import InviteTile from './InviteTile';
 import { SafeLink, LinkKind } from '../parser/types';
-import UserPreview from './UserPreview';
+import UserPreview, { WrappedInviterPreview } from './UserPreview';
 import EventPreview from './EventPreview';
 import HomeserverOptions from './HomeserverOptions';
 import DefaultPreview from './DefaultPreview';
+import Toggle from './Toggle';
 import { clientMap } from '../clients';
 import {
     getRoomFromId,
@@ -32,12 +33,7 @@ import {
     getUser,
 } from '../utils/cypher-wrapper';
 import { ClientContext } from '../contexts/ClientContext';
-import HSContext, {
-    TempHSContext,
-    HSOptions,
-    State as HSState,
-} from '../contexts/HSContext';
-import Toggle from './Toggle';
+import useHSs from '../utils/getHS';
 
 interface IProps {
     link: SafeLink;
@@ -51,13 +47,12 @@ const invite = async ({
     link: SafeLink;
 }): Promise<JSX.Element> => {
     // TODO: replace with client fetch
-    const defaultClient = await client(clientAddress);
     switch (link.kind) {
         case LinkKind.Alias:
             return (
                 <RoomPreviewWithTopic
                     room={
-                        await getRoomFromAlias(defaultClient, link.identifier)
+                        await getRoomFromAlias(clientAddress, link.identifier)
                     }
                 />
             );
@@ -65,14 +60,14 @@ const invite = async ({
         case LinkKind.RoomId:
             return (
                 <RoomPreviewWithTopic
-                    room={await getRoomFromId(defaultClient, link.identifier)}
+                    room={await getRoomFromId(clientAddress, link.identifier)}
                 />
             );
 
         case LinkKind.UserId:
             return (
                 <UserPreview
-                    user={await getUser(defaultClient, link.identifier)}
+                    user={await getUser(clientAddress, link.identifier)}
                     userId={link.identifier}
                 />
             );
@@ -80,10 +75,10 @@ const invite = async ({
         case LinkKind.Permalink:
             return (
                 <EventPreview
-                    room={await getRoomFromPermalink(defaultClient, link)}
+                    room={await getRoomFromPermalink(clientAddress, link)}
                     event={
                         await getEvent(
-                            defaultClient,
+                            await client(clientAddress),
                             link.roomLink,
                             link.eventId
                         )
@@ -118,32 +113,13 @@ const Preview: React.FC<PreviewProps> = ({ link, client }: PreviewProps) => {
     return content;
 };
 
-function selectedClient(link: SafeLink, hsOptions: HSState): string[] {
-    switch (hsOptions.option) {
-        case HSOptions.Unset:
-            return [];
-        case HSOptions.None:
-            return [];
-        case HSOptions.TrustedHSOnly:
-            return [hsOptions.hs];
-        case HSOptions.Any:
-            return [
-                'https://' + link.identifier.split(':')[1],
-                ...link.arguments.vias,
-            ];
-    }
-}
-
 const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
     let content: JSX.Element;
     const [showHSOptions, setShowHSOPtions] = useState(false);
-    const [hsOptions] = useContext(HSContext);
-    const [tempHSState] = useContext(TempHSContext);
 
-    if (
-        hsOptions.option === HSOptions.Unset &&
-        tempHSState.option === HSOptions.Unset
-    ) {
+    const hses = useHSs(link);
+
+    if (!hses.length) {
         content = (
             <>
                 <DefaultPreview link={link} />
@@ -151,7 +127,7 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
                     checked={showHSOptions}
                     onChange={(): void => setShowHSOPtions(!showHSOptions)}
                 >
-                    Show more information
+                    About {link.identifier}
                 </Toggle>
             </>
         );
@@ -159,16 +135,12 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
             content = (
                 <>
                     {content}
-                    <HomeserverOptions />
+                    <HomeserverOptions link={link} />
                 </>
             );
         }
     } else {
-        const clients =
-            tempHSState.option !== HSOptions.Unset
-                ? selectedClient(link, tempHSState)
-                : selectedClient(link, hsOptions);
-        content = <Preview link={link} client={clients[0]} />;
+        content = <Preview link={link} client={hses[0]} />;
     }
 
     const [{ clientId }] = useContext(ClientContext);
@@ -182,8 +154,22 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
 
     const client = displayClientId ? clientMap[displayClientId] : null;
 
+    const sharer = link.arguments.sharer ? (
+        <WrappedInviterPreview
+            link={{
+                kind: LinkKind.UserId,
+                identifier: link.arguments.sharer,
+                arguments: { vias: [] },
+                originalLink: '',
+            }}
+        />
+    ) : (
+        <p style={{ margin: '0 0 10px 0' }}>You're invited to join</p>
+    );
+
     return (
         <InviteTile client={client} link={link}>
+            {sharer}
             {content}
         </InviteTile>
     );
diff --git a/src/components/MatrixTile.tsx b/src/components/MatrixTile.tsx
index ee7ebaa7172dbde07dede5ef6fd4c4250342ecc6..643e830e57ed3740737f17cf4c0f6d0349e402a5 100644
--- a/src/components/MatrixTile.tsx
+++ b/src/components/MatrixTile.tsx
@@ -21,15 +21,30 @@ import logo from '../imgs/matrix-logo.svg';
 
 import './MatrixTile.scss';
 
-const MatrixTile: React.FC = () => {
+interface IProps {
+    isLink?: boolean;
+}
+
+const MatrixTile: React.FC<IProps> = ({ isLink }: IProps) => {
+    const copy = isLink ? (
+        <div>
+            This invite uses <a href="https://matrix.org">Matrix</a>, an open
+            network for secure, decentralized communication.
+        </div>
+    ) : (
+        <div>
+            Matrix.to is a stateless URL redirecting service for the{' '}
+            <a href="https://matrix.org">Matrix</a> ecosystem.
+        </div>
+    );
+
     return (
-        <Tile className="matrixTile">
-            <img src={logo} alt="matrix-logo" />
-            <div>
-                This invite uses <a href="https://matrix.org">Matrix</a>, an
-                open network for secure, decentralized communication.
-            </div>
-        </Tile>
+        <div>
+            <Tile className="matrixTile">
+                <img src={logo} alt="matrix-logo" />
+                {copy}
+            </Tile>
+        </div>
     );
 };
 
diff --git a/src/components/RoomPreview.scss b/src/components/RoomPreview.scss
index ef82fb94f3f093c5ed83e6004972d7b88c8e9d48..b7e7b367214d445ea1e8fa27d9c82b5201bd1cd6 100644
--- a/src/components/RoomPreview.scss
+++ b/src/components/RoomPreview.scss
@@ -16,16 +16,15 @@ limitations under the License.
 
 .roomPreview {
     > .avatar {
-        margin-top: 20px;
-        margin-bottom: 16px;
+        margin-bottom: 8px;
     }
 
     > h1 {
-        font-size: 20px;
+        font-size: 24px;
         margin-bottom: 4px;
     }
 }
 
 .roomTopic {
-    padding-top: 32px;
+    padding-top: 8px;
 }
diff --git a/src/components/RoomPreview.tsx b/src/components/RoomPreview.tsx
index e7e54044e0b2208332e858a30a98bc9caccb7b99..0ef2f14c61043d17326815267ef678b856a61647 100644
--- a/src/components/RoomPreview.tsx
+++ b/src/components/RoomPreview.tsx
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 import React from 'react';
-import { Room } from 'matrix-cypher';
+import { Room } from '../matrix-cypher';
 
 import { RoomAvatar } from './Avatar';
 
@@ -31,11 +31,15 @@ const RoomPreview: React.FC<IProps> = ({ room }: IProps) => {
         : room.aliases
         ? room.aliases[0]
         : room.room_id;
+    const members =
+        room.num_joined_members > 0 ? (
+            <p>{room.num_joined_members.toLocaleString()} members</p>
+        ) : null;
     return (
         <div className="roomPreview">
             <RoomAvatar room={room} />
             <h1>{room.name ? room.name : roomAlias}</h1>
-            <p>{room.num_joined_members.toLocaleString()} members</p>
+            {members}
             <p>{roomAlias}</p>
         </div>
     );
diff --git a/src/components/StyledCheckbox.tsx b/src/components/StyledCheckbox.tsx
index cd39a375ad1853087ad9ef13d5460e55877e58ad..9450f4e0fd3e66db40b8a264faf2f403bf45bdba 100644
--- a/src/components/StyledCheckbox.tsx
+++ b/src/components/StyledCheckbox.tsx
@@ -32,7 +32,7 @@ const StyledCheckbox: React.FC<IProps> = ({
         <input {...otherProps} type="checkbox" />
         {/* Using the div to center the image */}
         <div className="styledCheckboxWrapper">
-            <img src={tick} />
+            <img src={tick} alt="" />
         </div>
         {children}
     </label>
diff --git a/src/components/TextButton.scss b/src/components/TextButton.scss
index a5168e04a5f7ae29215f0a75dbdc34c56ada7b9e..cdaed546aa8eeae897a7302c344dd923b9b13ea2 100644
--- a/src/components/TextButton.scss
+++ b/src/components/TextButton.scss
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-@import "../color-scheme";
+@import '../color-scheme';
 
 .textButton {
     background: none;
@@ -24,4 +24,8 @@ limitations under the License.
     font-weight: normal;
     font-size: 14px;
     line-height: 24px;
+
+    &:hover {
+        cursor: pointer;
+    }
 }
diff --git a/src/components/Tile.scss b/src/components/Tile.scss
index a18f8ad8db3a94f1d6f60d5a3da980d6003f2b87..a5dabae773add200e31602581c69e5a29d274f75 100644
--- a/src/components/Tile.scss
+++ b/src/components/Tile.scss
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-@import "../color-scheme";
+@import '../color-scheme';
 
 .tile {
     background-color: $background;
@@ -30,4 +30,5 @@ limitations under the License.
     p {
         color: $grey;
     }
+    transition: width 2s, height 2s, transform 2s;
 }
diff --git a/src/components/Toggle.scss b/src/components/Toggle.scss
index e4601b285aae18d162e498ac8152a006561dbbb3..5d8222303c4d27363f4b58d5a1a876d2b54ec681 100644
--- a/src/components/Toggle.scss
+++ b/src/components/Toggle.scss
@@ -37,4 +37,8 @@ limitations under the License.
             }
         }
     }
+
+    &:hover {
+        cursor: pointer;
+    }
 }
diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx
index eba19521c8935ed5701f0379dd90840dcc6edf83..496a758db94c124fb9032ac85fbaf6dce481d3f5 100644
--- a/src/components/Toggle.tsx
+++ b/src/components/Toggle.tsx
@@ -21,14 +21,14 @@ import chevron from '../imgs/chevron-down.svg';
 import './Toggle.scss';
 
 interface IProps extends React.InputHTMLAttributes<Element> {
-    children?: React.ReactChild;
+    children?: React.ReactNode;
 }
 
 const Toggle: React.FC<IProps> = ({ children, ...props }: IProps) => (
     <label className="toggle">
         {children}
         <input type="checkbox" {...props} />
-        <img src={chevron} />
+        <img src={chevron} alt="" />
     </label>
 );
 
diff --git a/src/components/UserPreview.scss b/src/components/UserPreview.scss
index d35dfdab67968e3243fa29ec69322445567bfd84..c711e5a4f793955dee48a47566cf19c29dbfc60d 100644
--- a/src/components/UserPreview.scss
+++ b/src/components/UserPreview.scss
@@ -14,18 +14,17 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-@import "../color-scheme";
+@import '../color-scheme';
 
 .userPreview {
     width: 100%;
 
     > .avatar {
-        margin-top: 20px;
-        margin-bottom: 16px;
+        margin-bottom: 8px;
     }
 
     h1 {
-        font-size: 20px;
+        font-size: 24px;
         margin-bottom: 4px;
     }
 
@@ -70,5 +69,17 @@ limitations under the License.
     .avatar {
         flex-grow: 0;
         flex-shrink: 0;
+        height: 32px;
+        width: 32px;
+    }
+
+    &.centeredMiniUserPreview {
+        h1 {
+            width: unset;
+            text-align: center;
+        }
+        img {
+            display: none;
+        }
     }
 }
diff --git a/src/components/UserPreview.tsx b/src/components/UserPreview.tsx
index 415b2a226c25eaf4a4d1259611a21e840e329234..f0928c875553340a29b01d8d35f94682177d9442 100644
--- a/src/components/UserPreview.tsx
+++ b/src/components/UserPreview.tsx
@@ -14,10 +14,14 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
-import { User } from 'matrix-cypher';
+import React, { useState, useEffect } from 'react';
+import { client, User, getUserDetails } from '../matrix-cypher';
+import classNames from 'classnames';
+import icon from '../imgs/chat-icon.svg';
 
-import { UserAvatar } from './Avatar';
+import Avatar, { UserAvatar } from './Avatar';
+import useHSs from '../utils/getHS';
+import { UserId } from '../parser/types';
 
 import './UserPreview.scss';
 
@@ -37,14 +41,57 @@ const UserPreview: React.FC<IProps> = ({ user, userId }: IProps) => (
 
 export default UserPreview;
 
-export const InviterPreview: React.FC<IProps> = ({ user, userId }: IProps) => (
-    <div className="miniUserPreview">
-        <div>
-            <h1>
-                Invited by <b>{user.displayname}</b>
-            </h1>
-            <p>{userId}</p>
-        </div>
+interface InviterPreviewProps {
+    user?: User;
+    userId: string;
+}
+
+export const InviterPreview: React.FC<InviterPreviewProps> = ({
+    user,
+    userId,
+}: InviterPreviewProps) => {
+    const avatar = user ? (
         <UserAvatar user={user} userId={userId} />
-    </div>
-);
+    ) : (
+        <Avatar
+            className="avatarNoCrop"
+            label={`Placeholder icon for ${userId}`}
+            avatarUrl={icon}
+        />
+    );
+    const className = classNames('miniUserPreview', {
+        centeredMiniUserPreview: !user,
+    });
+
+    return (
+        <div className={className}>
+            <div>
+                <h1>
+                    Invited by <b>{user ? user.displayname : userId}</b>
+                </h1>
+                {user ? <p>{userId}</p> : null}
+            </div>
+            {avatar}
+        </div>
+    );
+};
+
+interface WrappedInviterProps {
+    link: UserId;
+}
+
+export const WrappedInviterPreview: React.FC<WrappedInviterProps> = ({
+    link,
+}: WrappedInviterProps) => {
+    const [user, setUser] = useState<User | undefined>(undefined);
+    const hss = useHSs(link);
+    useEffect(() => {
+        if (hss.length) {
+            client(hss[0])
+                .then((c) => getUserDetails(c, link.identifier))
+                .then(setUser)
+                .catch((x) => console.log("couldn't fetch user preview", x));
+        }
+    }, [hss, link]);
+    return <InviterPreview user={user} userId={link.identifier} />;
+};
diff --git a/src/contexts/ClientContext.ts b/src/contexts/ClientContext.ts
index 0c78807140da6875e4139da71228aa8b18698389..6fb364b80528ba548f0d9f964520a44a6974494c 100644
--- a/src/contexts/ClientContext.ts
+++ b/src/contexts/ClientContext.ts
@@ -101,6 +101,8 @@ export const ClientContext = React.createContext<
     [State, React.Dispatch<Action>]
 >([initialState, (): void => {}]);
 
+export default ClientContext;
+
 // Quick rename to make importing easier
 export const ClientProvider = ClientContext.Provider;
 export const ClientConsumer = ClientContext.Consumer;
diff --git a/src/contexts/HSContext.ts b/src/contexts/HSContext.ts
index 686ba05514e96909f18e3ec6a5ae8c05d4213482..4105af697fa290f9a884a6ecd4c8d20e01602c14 100644
--- a/src/contexts/HSContext.ts
+++ b/src/contexts/HSContext.ts
@@ -19,8 +19,6 @@ import { string, object, union, literal, TypeOf } from 'zod';
 
 import { persistReducer } from '../utils/localStorage';
 
-//import { prefixFetch, Client, discoverServer } from 'matrix-cypher';
-
 export enum HSOptions {
     // The homeserver contact policy hasn't
     // been set yet.
@@ -29,17 +27,12 @@ export enum HSOptions {
     TrustedHSOnly = 'TRUSTED_CLIENT_ONLY',
     // Matrix.to may contact any homeserver it requires
     Any = 'ANY',
-    // Matrix.to may not contact any homeservers
-    None = 'NONE',
 }
 
 const STATE_SCHEMA = union([
     object({
         option: literal(HSOptions.Unset),
     }),
-    object({
-        option: literal(HSOptions.None),
-    }),
     object({
         option: literal(HSOptions.Any),
     }),
@@ -55,7 +48,7 @@ export type State = TypeOf<typeof STATE_SCHEMA>;
 export enum ActionType {
     SetHS = 'SET_HS',
     SetAny = 'SET_ANY',
-    SetNone = 'SET_NONE',
+    Clear = 'CLEAR',
 }
 
 export interface SetHS {
@@ -67,24 +60,18 @@ export interface SetAny {
     action: ActionType.SetAny;
 }
 
-export interface SetNone {
-    action: ActionType.SetNone;
+export interface Clear {
+    action: ActionType.Clear;
 }
 
-export type Action = SetHS | SetAny | SetNone;
+export type Action = SetHS | SetAny | Clear;
 
 export const INITIAL_STATE: State = {
     option: HSOptions.Unset,
 };
 
-export const unpersistedReducer = (state: State, action: Action): State => {
-    console.log('reducing');
-    console.log(action);
+export const unpersistedReducer = (_state: State, action: Action): State => {
     switch (action.action) {
-        case ActionType.SetNone:
-            return {
-                option: HSOptions.None,
-            };
         case ActionType.SetAny:
             return {
                 option: HSOptions.Any,
@@ -94,8 +81,10 @@ export const unpersistedReducer = (state: State, action: Action): State => {
                 option: HSOptions.TrustedHSOnly,
                 hs: action.HSURL,
             };
-        default:
-            return state;
+        case ActionType.Clear:
+            return {
+                option: HSOptions.Unset,
+            };
     }
 };
 
diff --git a/src/imgs/copy.svg b/src/imgs/copy.svg
new file mode 100644
index 0000000000000000000000000000000000000000..79069746a3a49c82fbc8815a33c98679fe5ef779
--- /dev/null
+++ b/src/imgs/copy.svg
@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5 15H6C4.89543 15 4 14.1046 4 13V6C4 4.89543 4.89543 4 6 4H13C14.1046 4 15 4.89543 15 6V9.5" stroke="white" stroke-width="1.5"/>
+<rect x="9" y="9" width="11" height="11" rx="2" stroke="white" stroke-width="1.5"/>
+</svg>
diff --git a/src/imgs/fractal.png b/src/imgs/fractal.png
new file mode 100644
index 0000000000000000000000000000000000000000..e60c89c98878256859f41e0667183cccf6de2d68
Binary files /dev/null and b/src/imgs/fractal.png differ
diff --git a/src/imgs/link.svg b/src/imgs/link.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f41a6737542e336308343b88e98807c6051b5ccd
--- /dev/null
+++ b/src/imgs/link.svg
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.5285 6.54089L13.0273 6.04207C14.4052 4.66426 16.6259 4.65104 17.9874 6.01253C19.349 7.37402 19.3357 9.59466 17.9579 10.9725L15.5878 13.3425C14.21 14.7203 11.9893 14.7335 10.6277 13.372M11.4717 17.4589L10.9727 17.9579C9.59481 19.3357 7.37409 19.349 6.01256 17.9875C4.65102 16.626 4.66426 14.4053 6.04211 13.0275L8.41203 10.6577C9.78988 9.27988 12.0106 9.26665 13.3721 10.6281" stroke="white" stroke-width="2" stroke-linecap="round"/>
+</svg>
diff --git a/src/imgs/nheko.svg b/src/imgs/nheko.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ce3ec4069ee57e903f4759a0e2c4e42afb43943b
--- /dev/null
+++ b/src/imgs/nheko.svg
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="1024"
+   height="1024"
+   viewBox="0 0 270.93333 270.93333"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.4 5da689c313, 2019-01-14"
+   sodipodi:docname="nheko.svg"
+   inkscape:export-filename="/home/nicolas/Dokumente/devel/open-source/nheko/resources/nheko-rebuild-round-corners.svg.png"
+   inkscape:export-xdpi="130.048"
+   inkscape:export-ydpi="130.048">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.35355339"
+     inkscape:cx="852.07808"
+     inkscape:cy="-60.410565"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer2"
+     showgrid="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1019"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     showguides="true"
+     inkscape:snap-grids="true"
+     gridtolerance="10"
+     inkscape:snap-bbox="false"
+     inkscape:bbox-paths="true"
+     inkscape:snap-global="true"
+     inkscape:bbox-nodes="true"
+     inkscape:lockguides="false"
+     units="px">
+    <sodipodi:guide
+       position="0,0"
+       orientation="0,793.70079"
+       id="guide4797"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       position="0,297"
+       orientation="1122.5197,0"
+       id="guide4803"
+       inkscape:locked="false" />
+    <inkscape:grid
+       type="axonomgrid"
+       id="grid4805"
+       units="px"
+       empspacing="2"
+       snapvisiblegridlinesonly="true"
+       spacingy="1.0583333" />
+    <sodipodi:guide
+       position="0,0"
+       orientation="0,755.90551"
+       id="guide4807"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       position="200,0"
+       orientation="-755.90551,0"
+       id="guide4809"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       position="200,200"
+       orientation="0,-755.90551"
+       id="guide4811"
+       inkscape:locked="false" />
+    <inkscape:grid
+       type="xygrid"
+       id="grid871"
+       empspacing="2"
+       color="#d43fff"
+       opacity="0.1254902"
+       empcolor="#cf3fff"
+       empopacity="0.25098039"
+       units="px"
+       spacingx="1.0583333"
+       spacingy="1.0583333"
+       enabled="false" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:groupmode="layer"
+     id="layer2"
+     inkscape:label="Logo"
+     style="display:inline"
+     transform="translate(0,-26.066668)">
+    <circle
+       id="path3792"
+       cx="135.46666"
+       cy="161.53333"
+       style="display:inline;fill:#333333;fill-opacity:1;stroke:none;stroke-width:0.3584221"
+       inkscape:transform-center-x="-57.929751"
+       inkscape:transform-center-y="532.03976"
+       inkscape:export-xdpi="96.000008"
+       inkscape:export-ydpi="96.000008"
+       r="135.46666" />
+    <path
+       style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.32663074px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 48.965212,110.73276 H 239.52342 c 4.88824,0 4.88824,0 0,8.46688 L 180.59519,221.2662 c -4.6188,8.00001 -4.6188,8.00001 -9.50702,8.00001 h -19.55294 c -4.88824,0 -4.88824,0 -0.26944,-8.00001 l 44.2635,-76.66608 h -29.41224 l -43.91123,76.19952 c -4.88823,8.46657 -4.88823,8.46657 -9.77646,8.46657 H 29.329398 l 49.299816,-84.66609 h -49.29982 z"
+       id="path4834"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccccccccccccc"
+       inkscape:export-xdpi="96.000008"
+       inkscape:export-ydpi="96.000008" />
+    <path
+       style="fill:#c0def5;fill-opacity:1;stroke:none;stroke-width:0.3584221px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 97.764652,110.73276 H 127.09406 L 58.658797,229.26621 H 29.329398 Z"
+       id="path4836"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc"
+       inkscape:export-xdpi="96.000008"
+       inkscape:export-ydpi="96.000008" />
+    <path
+       style="fill:#87aade;fill-opacity:1;stroke:none;stroke-width:0.3584221px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 58.658797,229.26621 127.09406,110.73276 h 29.3294 L 87.988193,229.26621 Z"
+       id="path4838"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc"
+       inkscape:export-xdpi="96.000008"
+       inkscape:export-ydpi="96.000008" />
+  </g>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     style="display:inline"
+     transform="translate(0,-26.066668)" />
+</svg>
diff --git a/src/imgs/refresh.svg b/src/imgs/refresh.svg
new file mode 100644
index 0000000000000000000000000000000000000000..5463124327907958dfdb560344b7dfe63092119b
--- /dev/null
+++ b/src/imgs/refresh.svg
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M17.6498 6.35001C16.0198 4.72001 13.7098 3.78001 11.1698 4.04001C7.49978 4.41001 4.47978 7.39001 4.06978 11.06C3.51978 15.91 7.26978 20 11.9998 20C15.1898 20 17.9298 18.13 19.2098 15.44C19.5298 14.77 19.0498 14 18.3098 14C17.9398 14 17.5898 14.2 17.4298 14.53C16.2998 16.96 13.5898 18.5 10.6298 17.84C8.40978 17.35 6.61978 15.54 6.14978 13.32C5.30978 9.44001 8.25978 6.00001 11.9998 6.00001C13.6598 6.00001 15.1398 6.69001 16.2198 7.78001L14.7098 9.29001C14.0798 9.92001 14.5198 11 15.4098 11H18.9998C19.5498 11 19.9998 10.55 19.9998 10V6.41001C19.9998 5.52001 18.9198 5.07001 18.2898 5.70001L17.6498 6.35001Z" fill="white"/>
+</svg>
diff --git a/src/imgs/tick.svg b/src/imgs/tick.svg
index b49f4a4f75ce5720426faf76cc7432bef88f2a93..b4594909734d2a8b1eab2fe30a15e35072138629 100644
--- a/src/imgs/tick.svg
+++ b/src/imgs/tick.svg
@@ -1,3 +1,3 @@
-<svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M0.979065 4L3.63177 7L8.93718 1" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.5 12.5L8.84497 15.845C9.71398 16.714 11.1538 16.601 11.8767 15.6071L18.5 6.5" stroke="white" stroke-width="2" stroke-linecap="round"/>
 </svg>
diff --git a/src/index.scss b/src/index.scss
index a9b22d8629edaab6f1da73e81245b5a9e20e5a0e..0fcbaa813810567203268f888a7f936406552f13 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -47,6 +47,7 @@ h1 {
     font-size: 24px;
     line-height: 32px;
     text-align: center;
+    color: $foreground;
 }
 
 h4 {
diff --git a/src/matrix-cypher/index.ts b/src/matrix-cypher/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8bdc9331def67e4df947a21300bba65a5c55ef39
--- /dev/null
+++ b/src/matrix-cypher/index.ts
@@ -0,0 +1,19 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+export * from './matrix-cypher';
+export * from './utils';
+export * from './schemas';
diff --git a/src/matrix-cypher/matrix-cypher.ts b/src/matrix-cypher/matrix-cypher.ts
new file mode 100644
index 0000000000000000000000000000000000000000..75387a2047fcf749dd44b0f532395fedaf91871d
--- /dev/null
+++ b/src/matrix-cypher/matrix-cypher.ts
@@ -0,0 +1,193 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the 'License');
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an 'AS IS' BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/* eslint-disable import/first */
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+import any from 'promise.any';
+any.shim()
+
+import VersionSchema from './schemas/VersionSchema';
+import WellKnownSchema from './schemas/WellKnownSchema';
+import UserSchema, { User } from './schemas/UserSchema';
+import RoomAliasSchema, {
+    RoomAlias,
+} from './schemas/RoomAliasSchema';
+import PublicRoomsSchema, {
+    PublicRooms,
+    Room,
+} from './schemas/PublicRoomsSchema';
+import EventSchema, {
+    Event,
+} from './schemas/EventSchema';
+import { ensure } from './utils/promises';
+import { prefixFetch, parseJSON } from './utils/fetch';
+
+
+/*
+ * A client is a resolved homeserver name wrapped in a lambda'd fetch
+ */
+export type Client = (path: string) => Promise<Response>;
+
+/*
+ * Confirms that the target homeserver is properly configured and operational
+ */
+export const validateHS = (host: string) =>
+    prefixFetch(host)('/_matrix/client/versions')
+        .then(parseJSON)
+        .then(VersionSchema.parse)
+        .then(() => host);
+
+/*
+ * Discovers the correct domain name for the host according to the spec's
+ * discovery rules
+ */
+export const discoverServer = (host: string) =>
+    prefixFetch(host)('/.well-known/matrix/client')
+        .then(resp => resp.ok
+            ? resp.json()
+                .then(WellKnownSchema.parse)
+                .then(content => {
+                    if (content === undefined) return host;
+                    else if (
+                        'm.homeserver' in content && content['m.homeserver']
+                    ) {
+                        return content['m.homeserver'].base_url
+                    } else {
+                        return host
+                    }
+                })
+            : ensure(
+                resp.status === 404,
+                () => host,
+            ),
+        )
+        .then(validateHS)
+
+
+/*
+ * Takes a hs domain and resolves it to it's current domain and returns a
+ * client
+ */
+export async function client(host: string): Promise<Client> {
+    return prefixFetch(await discoverServer(host))
+}
+
+/*
+ * Gets the details for a user
+ */
+export function getUserDetails(
+    client: Client,
+    userId: string,
+): Promise<User> {
+    return client(`/_matrix/client/r0/profile/${userId}`)
+        .then(parseJSON)
+        .then(UserSchema.parse)
+}
+
+/*
+ * Gets the roomId of a room by resolving it's alias
+ */
+export function getRoomIdFromAlias(
+    client: Client,
+    roomAlias: string,
+): Promise<RoomAlias> {
+    const encodedRoomAlias = encodeURIComponent(roomAlias);
+    return client(`/_matrix/client/r0/directory/room/${encodedRoomAlias}`)
+        .then(parseJSON)
+        .then(RoomAliasSchema.parse);
+}
+
+/*
+ * Gets the details of a room if that room is public
+ */
+export function getRoomDetails(clients: Client[], roomId: string): Promise<Room> {
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore
+    return Promise.any(clients.map(client => searchPublicRooms(client, roomId)));
+}
+
+/*
+ * Gets a list of all public rooms on a hs
+ */
+export function getPublicRooms(client: Client): Promise<PublicRooms> {
+    return getPublicRoomsUnsafe(client)
+        .then(PublicRoomsSchema.parse)
+}
+
+/*
+ * Similar to getPubliRooms however id doesn't confirm the data returned from
+ * the hs is correct
+ *
+ * This is used because the room list can be huge and validating it all takes
+ * a long time
+ */
+export function getPublicRoomsUnsafe(client: Client): Promise<PublicRooms> {
+    // TODO: Do not assume server will return all results in one go
+    return client('/_matrix/client/r0/publicRooms')
+        .then(parseJSON)
+}
+
+/*
+ * Searches the public rooms of a homeserver for the metadata of a particular
+ */
+export function searchPublicRooms(
+    client: Client,
+    roomId: string,
+): Promise<Room> {
+    // we use the unsage version here because the safe one is sloooow
+    return getPublicRoomsUnsafe(client)
+        .then(rooms => {
+            const [match] = rooms.chunk.filter(
+                chunk => chunk.room_id === roomId,
+            );
+            return match !== undefined
+                ? Promise.resolve(match)
+                : Promise.reject(new Error(
+                    `This server knowns no public room with id ${roomId}`,
+                ));
+        });
+}
+
+/*
+ * Gets the details of an event from the homeserver
+ */
+export async function getEvent(
+    client: Client,
+    roomIdOrAlias: string,
+    eventId: string,
+): Promise<Event> {
+    return client(`/_matrix/client/r0/rooms/${roomIdOrAlias}/event/${eventId}`)
+        .then(parseJSON)
+        .then(EventSchema.parse);
+}
+
+/*
+ * Gets an mxc resource
+ */
+export function convertMXCtoMediaQuery(
+    clientURL: string,
+    mxc: string,
+): string {
+    // mxc://matrix.org/EqMZYbAYhREvHXvYFyfxOlkf
+    const matches = mxc.match(/mxc:\/\/(.+)\/(.+)/)
+    if (!matches) {
+        throw new Error(`mxc invalid: ${JSON.stringify({mxc})}`);
+    }
+
+    return `${clientURL}/_matrix/media/r0/download/${matches[1]}/${matches[2]}`;
+}
diff --git a/src/matrix-cypher/schemas/EventSchema.ts b/src/matrix-cypher/schemas/EventSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2b0402abcf47acadaba5e61a2a601d6ddc4b7f31
--- /dev/null
+++ b/src/matrix-cypher/schemas/EventSchema.ts
@@ -0,0 +1,30 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { object, string, TypeOf } from 'zod';
+
+const EventSchema = object({
+  content: object({}).nonstrict(),
+  type: string(),
+  event_id: string(),
+  sender: string(),
+  origin_server_ts: string(),
+  unsigned: object({}).nonstrict().optional(),
+  room_id: string(),
+});
+
+export type Event = TypeOf<typeof EventSchema>;
+export default EventSchema;
diff --git a/src/matrix-cypher/schemas/PublicRoomsSchema.ts b/src/matrix-cypher/schemas/PublicRoomsSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b33cb31cc63bc32fde32fafc78b709f102e69544
--- /dev/null
+++ b/src/matrix-cypher/schemas/PublicRoomsSchema.ts
@@ -0,0 +1,43 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { object, array, string, boolean, number, TypeOf } from 'zod';
+
+export const RoomSchema = object({
+  aliases: array(string()).optional(),
+  canonical_alias: string().optional(),
+  name: string().optional(),
+  num_joined_members: number(),
+  room_id: string(),
+  topic: string().optional(),
+  world_readable: boolean(),
+  guest_can_join: boolean(),
+  avatar_url: string().optional(),
+});
+
+
+const PublicRoomsSchema = object({
+  chunk: array(RoomSchema),
+  next_batch: string().optional(),
+  prev_batch: string().optional(),
+  total_room_count_estimate: number().optional(),
+});
+
+export type Room = TypeOf<typeof RoomSchema>;
+export type PublicRooms = TypeOf<typeof PublicRoomsSchema>;
+
+export default PublicRoomsSchema;
+
diff --git a/src/matrix-cypher/schemas/RoomAliasSchema.ts b/src/matrix-cypher/schemas/RoomAliasSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f662a4b3c04e6c1e4620d079c2fee0102b6189df
--- /dev/null
+++ b/src/matrix-cypher/schemas/RoomAliasSchema.ts
@@ -0,0 +1,26 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { object, array, string, TypeOf } from 'zod';
+
+const RoomAliasSchema = object({
+  room_id: string(),
+  servers: array(string()),
+});
+
+export type RoomAlias = TypeOf<typeof RoomAliasSchema>;
+export default RoomAliasSchema;
+
diff --git a/src/matrix-cypher/schemas/UserSchema.ts b/src/matrix-cypher/schemas/UserSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4702e8ea98a40eb5f02b8bd5218164779454e8b0
--- /dev/null
+++ b/src/matrix-cypher/schemas/UserSchema.ts
@@ -0,0 +1,26 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { object, string, TypeOf } from 'zod';
+
+const UserSchema = object({
+  avatar_url: string().optional(),
+  displayname: string().optional(),
+})
+
+export type User = TypeOf<typeof UserSchema>;
+export default UserSchema;
+
diff --git a/src/matrix-cypher/schemas/VersionSchema.ts b/src/matrix-cypher/schemas/VersionSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3af08c0ec0708cee0c5131f6fc7649cd35f44c30
--- /dev/null
+++ b/src/matrix-cypher/schemas/VersionSchema.ts
@@ -0,0 +1,21 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { object, string, array } from 'zod';
+
+export default object({
+  versions: array(string()),
+}).nonstrict()
diff --git a/src/matrix-cypher/schemas/WellKnownSchema.ts b/src/matrix-cypher/schemas/WellKnownSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7cfaa29e831b6d1b2985ee0ccaa1d03d7b25fa1f
--- /dev/null
+++ b/src/matrix-cypher/schemas/WellKnownSchema.ts
@@ -0,0 +1,29 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { object, string, TypeOf } from 'zod';
+
+const WellKnownSchema = object({
+  'm.homeserver': object({
+    'base_url': string().url(),
+  }),
+  'm.identity_server': object({
+    'base_url': string().url(),
+  }),
+});
+
+export type WellKnown = TypeOf<typeof WellKnownSchema>;
+export default WellKnownSchema;
diff --git a/src/matrix-cypher/schemas/index.ts b/src/matrix-cypher/schemas/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f7fa3c32a40b8a0ab33f28522c4178fda604680b
--- /dev/null
+++ b/src/matrix-cypher/schemas/index.ts
@@ -0,0 +1,24 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+export * from './EventSchema';
+export * from './PublicRoomsSchema';
+export * from './RoomAliasSchema';
+export * from './UserSchema';
+export * from './VersionSchema';
+export * from './WellKnownSchema';
+export * from './index';
+
diff --git a/src/matrix-cypher/utils/fetch.ts b/src/matrix-cypher/utils/fetch.ts
new file mode 100644
index 0000000000000000000000000000000000000000..44255fb0a0ce1f7c79c644408a65d80d7bbf0e79
--- /dev/null
+++ b/src/matrix-cypher/utils/fetch.ts
@@ -0,0 +1,36 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import fetch from 'cross-fetch';
+
+import { ensure } from './promises';
+
+/*
+ * Wraps a fetch with a domain for easy reuse.
+ */
+export function prefixFetch(host: string) {
+    return (path: string) => fetch(
+        new URL(path, host).toString(),
+    );
+}
+
+export function parseJSON(resp: Response) {
+    return ensure(
+        resp.ok,
+        () => resp.json(),
+        `Error from Homeserver. Error code: ${resp.status}`,
+    );
+}
diff --git a/src/matrix-cypher/utils/index.ts b/src/matrix-cypher/utils/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4e4adb1b9f3e750539e1102a89c8a5b53dd64a18
--- /dev/null
+++ b/src/matrix-cypher/utils/index.ts
@@ -0,0 +1,18 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+export * from './fetch';
+export * from './promises';
diff --git a/src/matrix-cypher/utils/promises.ts b/src/matrix-cypher/utils/promises.ts
new file mode 100644
index 0000000000000000000000000000000000000000..08ee36bceb606060d33f432a053d56746d3feee7
--- /dev/null
+++ b/src/matrix-cypher/utils/promises.ts
@@ -0,0 +1,60 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/*
+ * Conditional promises
+ */
+
+/*
+ * If the condition is false reject with rejectReason
+ * If it's true resolve with the result = resultThunk()
+ */
+export function ensure<T>(condition: boolean, resultThunk: () => T | PromiseLike<T>, rejectReason?: string) {
+    return condition
+        ? Promise.resolve(resultThunk())
+        : Promise.reject(new Error(rejectReason));
+}
+
+/*
+ * Loggin utilities
+ */
+
+/*
+ * Logs a then using "success: {label: successArg}"
+ */
+export function logThen<T>(label: string): (v: T) => T | PromiseLike<T> {
+    return (v: T) => {
+        console.log('success:', {[`${label}`]: v}); return v
+    }
+}
+
+/*
+ * Logs a catch using "fail: {label: failArg}"
+ */
+export function logCatch<T>(label: string): (v: T) => T | PromiseLike<T> {
+    return (v: T) => {
+        console.log('fail:', {[`${label}`]: v});
+        return Promise.reject(v)
+    }
+}
+
+/*
+ * inserts loggers for both callbacks of a then
+ */
+export function logThens<T1, T2 = T1>(label: string) {
+    return [logThen<T1>(label), logCatch<T2>(label)]
+}
+
diff --git a/src/pages/LinkRouter.tsx b/src/pages/LinkRouter.tsx
index a455c7d02c117b60c0920b08fdb8abca258982a0..cf3b4d674aa47ae3d8ea4615b64af142a32e231e 100644
--- a/src/pages/LinkRouter.tsx
+++ b/src/pages/LinkRouter.tsx
@@ -22,6 +22,8 @@ import InvitingClientTile from '../components/InvitingClientTile';
 import { parseHash } from '../parser/parser';
 import { LinkKind } from '../parser/types';
 
+/* eslint-disable no-restricted-globals */
+
 interface IProps {
     link: string;
 }
@@ -36,8 +38,14 @@ const LinkRouter: React.FC<IProps> = ({ link }: IProps) => {
         case LinkKind.ParseFailed:
             feedback = (
                 <Tile>
-                    <h1>Invalid matrix.to link</h1>
-                    <p>{link}</p>
+                    <p>
+                        That URL doesn't seem right. Links should be in the
+                        format:
+                    </p>
+                    <br />
+                    <p>
+                        {location.host}/#/{'<'}matrix-resourceidentifier{'>'}
+                    </p>
                 </Tile>
             );
             break;
@@ -53,7 +61,6 @@ const LinkRouter: React.FC<IProps> = ({ link }: IProps) => {
             feedback = (
                 <>
                     <LinkPreview link={parsedLink} />
-                    <hr />
                     {client}
                 </>
             );
diff --git a/src/utils/cypher-wrapper.ts b/src/utils/cypher-wrapper.ts
index 5a43a4cbdaf07e2de9ae528d5a0063cbc2fc5687..7e1093eedd09ab4f8c3ee7b6bcfa95cd5a98b189 100644
--- a/src/utils/cypher-wrapper.ts
+++ b/src/utils/cypher-wrapper.ts
@@ -20,6 +20,7 @@ limitations under the License.
 
 import {
     Client,
+    client,
     Room,
     RoomAlias,
     User,
@@ -27,7 +28,7 @@ import {
     searchPublicRooms,
     getUserDetails,
     convertMXCtoMediaQuery,
-} from 'matrix-cypher';
+} from '../matrix-cypher';
 import { LinkKind, Permalink } from '../parser/types';
 
 /* This is a collection of methods for providing fallback metadata
@@ -59,7 +60,8 @@ export const fallbackRoom = ({
     const roomAlias_ = roomAlias ? roomAlias : identifier;
     return {
         aliases: [roomAlias_],
-        topic: 'Unable to find room details.',
+        topic:
+            'No details available.  This might be a private room.  You can still join below.',
         canonical_alias: roomAlias_,
         name: roomAlias_,
         num_joined_members: 0,
@@ -75,18 +77,24 @@ export const fallbackRoom = ({
  * a `fallbackRoom`
  */
 export async function getRoomFromAlias(
-    client: Client,
+    clientURL: string,
     roomAlias: string
 ): Promise<Room> {
     let resolvedRoomAlias: RoomAlias;
+    let resolvedClient: Client;
+
     try {
-        resolvedRoomAlias = await getRoomIdFromAlias(client, roomAlias);
+        resolvedClient = await client(clientURL);
+        resolvedRoomAlias = await getRoomIdFromAlias(resolvedClient, roomAlias);
     } catch {
         return fallbackRoom({ identifier: roomAlias });
     }
 
     try {
-        return await searchPublicRooms(client, resolvedRoomAlias.room_id);
+        return await searchPublicRooms(
+            resolvedClient,
+            resolvedRoomAlias.room_id
+        );
     } catch {
         return fallbackRoom({
             identifier: roomAlias,
@@ -101,11 +109,12 @@ export async function getRoomFromAlias(
  * a `fallbackRoom`
  */
 export async function getRoomFromId(
-    client: Client,
+    clientURL: string,
     roomId: string
 ): Promise<Room> {
     try {
-        return await searchPublicRooms(client, roomId);
+        const resolvedClient = await client(clientURL);
+        return await searchPublicRooms(resolvedClient, roomId);
     } catch {
         return fallbackRoom({ identifier: roomId });
     }
@@ -114,9 +123,13 @@ export async function getRoomFromId(
 /*
  * Tries to fetch user details. If it fails it uses a `fallbackUser`
  */
-export async function getUser(client: Client, userId: string): Promise<User> {
+export async function getUser(
+    clientURL: string,
+    userId: string
+): Promise<User> {
     try {
-        return await getUserDetails(client, userId);
+        const resolvedClient = await client(clientURL);
+        return await getUserDetails(resolvedClient, userId);
     } catch {
         return fallbackUser(userId);
     }
@@ -127,7 +140,7 @@ export async function getUser(client: Client, userId: string): Promise<User> {
  * a `fallbackRoom`
  */
 export async function getRoomFromPermalink(
-    client: Client,
+    client: string,
     link: Permalink
 ): Promise<Room> {
     switch (link.roomKind) {
diff --git a/src/utils/getHS.ts b/src/utils/getHS.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b8239da45d2c8121f338e474a3469afa72acf677
--- /dev/null
+++ b/src/utils/getHS.ts
@@ -0,0 +1,52 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { useContext } from 'react';
+import HSContext, {
+    TempHSContext,
+    State,
+    HSOptions,
+} from '../contexts/HSContext';
+import { SafeLink } from '../parser/types';
+
+function selectedClient(link: SafeLink, hsOptions: State): string[] {
+    switch (hsOptions.option) {
+        case HSOptions.Unset:
+            return [];
+        case HSOptions.TrustedHSOnly:
+            return [hsOptions.hs];
+        case HSOptions.Any:
+            return [
+                ...link.identifier
+                    .split('/')
+                    .map((i) => 'https://' + i.split(':')[1]),
+                ...link.arguments.vias,
+            ];
+    }
+}
+
+export default function useHSs(link: SafeLink): string[] {
+    const [HSState] = useContext(HSContext);
+    const [TempHSState] = useContext(TempHSContext);
+
+    if (HSState.option !== HSOptions.Unset) {
+        return selectedClient(link, HSState);
+    } else if (TempHSState.option !== HSOptions.Unset) {
+        return selectedClient(link, TempHSState);
+    } else {
+        return [];
+    }
+}