diff --git a/package.json b/package.json
index 0b20ee7138b79a02aa854641d1c6d199a08bdcf2..7ae716823925338d25045fbf86cfb55b4dee7573 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
     "react-device-detect": "^1.17.0",
     "react-helmet": "^6.1.0",
     "react-hook-form": "6.3.0",
-    "react-overlapping-panels": "1.1.2-patch.0",
+    "react-overlapping-panels": "1.2.1",
     "react-redux": "^7.2.4",
     "react-router-dom": "^5.2.0",
     "redux": "^4.1.0",
diff --git a/src/app.tsx b/src/app.tsx
index 28e68f59f841acf8f51c85de0ca8bdbd6abd8f4a..ee5597930c1efa23a5aca733bb982f5570d92b84 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,40 +1,31 @@
 import { CheckAuth } from "./context/revoltjs/CheckAuth";
+import Preloader from "./components/ui/Preloader";
 import { Route, Switch } from "react-router-dom";
 import Context from "./context";
 
-import { Login } from "./pages/login/Login";
-
-import { useForceUpdate, useSelf, useUser } from "./context/revoltjs/hooks";
-
-function Test() {
-    const ctx = useForceUpdate();
-
-    let self = useSelf(ctx);
-    let bree = useUser('01EZZJ98RM1YVB1FW9FG221CAN', ctx);
-
-    return (
-        <div>
-            <h1>logged in as { self?.username }</h1>
-            <h4>bree: { JSON.stringify(bree) }</h4>
-        </div>
-    )
-}
+import { lazy, Suspense } from "preact/compat";
+const Login = lazy(() => import('./pages/login/Login'));
+const RevoltApp = lazy(() => import('./pages/App'));
 
 export function App() {
     return (
         <Context>
-            <Switch>
-                <Route path="/login">
-                    <CheckAuth>
-                        <Login />
-                    </CheckAuth>
-                </Route>
-                <Route path="/">
-                    <CheckAuth auth>
-                        <Test />
-                    </CheckAuth>
-                </Route>
-            </Switch>
+            {/* 
+            // @ts-expect-error */}
+            <Suspense fallback={<Preloader />}>
+                <Switch>
+                    <Route path="/login">
+                        <CheckAuth>
+                            <Login />
+                        </CheckAuth>
+                    </Route>
+                    <Route path="/">
+                        <CheckAuth auth>
+                            <RevoltApp />
+                        </CheckAuth>
+                    </Route>
+                </Switch>
+            </Suspense>
         </Context>
     );
 }
diff --git a/src/components/common/ChannelIcon.tsx b/src/components/common/ChannelIcon.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8be9fe8085e234444d9c20ddf99116f83029176a
--- /dev/null
+++ b/src/components/common/ChannelIcon.tsx
@@ -0,0 +1,45 @@
+import { useContext } from "preact/hooks";
+import { Hash } from "@styled-icons/feather";
+import IconBase, { IconBaseProps } from "./IconBase";
+import { Channels } from "revolt.js/dist/api/objects";
+import { AppContext } from "../../context/revoltjs/RevoltClient";
+
+interface Props extends IconBaseProps<Channels.GroupChannel | Channels.TextChannel> {
+    isServerChannel?: boolean;
+}
+
+const fallback = '/assets/group.png';
+export default function ChannelIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>) {
+    const { client } = useContext(AppContext);
+
+    const { size, target, attachment, isServerChannel: server, animate, children, as, ...svgProps } = props;
+    const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate);
+    const isServerChannel = server || target?.channel_type === 'TextChannel';
+
+    if (typeof iconURL === 'undefined') {
+        if (isServerChannel) {
+            return (
+                <Hash size={size} />
+            )
+        }
+    }
+
+    return (
+        <IconBase {...svgProps}
+            width={size}
+            height={size}
+            aria-hidden="true"
+            viewBox="0 0 32 32"
+            square={isServerChannel}>
+            <foreignObject x="0" y="0" width="32" height="32">
+                <img src={iconURL ?? fallback}
+                    onError={ e => {
+                        let el = e.currentTarget;
+                        if (el.src !== fallback) {
+                            el.src = fallback
+                        }
+                    } } />
+            </foreignObject>
+        </IconBase>
+    );
+}
diff --git a/src/components/common/IconBase.tsx b/src/components/common/IconBase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..fb1f75bdb6bf450576ee0d7e4c5c60318a5e6c16
--- /dev/null
+++ b/src/components/common/IconBase.tsx
@@ -0,0 +1,22 @@
+import { Attachment } from "revolt.js/dist/api/objects";
+import styled, { css } from "styled-components";
+
+export interface IconBaseProps<T> {
+    target?: T;
+    attachment?: Attachment;
+
+    size: number;
+    animate?: boolean;
+}
+
+export default styled.svg<{ square?: boolean }>`
+    img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+
+        ${ props => !props.square && css`
+            border-radius: 50%;
+        ` }
+    }
+`;
diff --git a/src/components/common/ServerIcon.tsx b/src/components/common/ServerIcon.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1cf0297f445bb440d6a4ea14c4902f046e9e3002
--- /dev/null
+++ b/src/components/common/ServerIcon.tsx
@@ -0,0 +1,57 @@
+import styled from "styled-components";
+import { useContext } from "preact/hooks";
+import { Server } from "revolt.js/dist/api/objects";
+import IconBase, { IconBaseProps } from "./IconBase";
+import { AppContext } from "../../context/revoltjs/RevoltClient";
+
+interface Props extends IconBaseProps<Server> {
+    server_name?: string;
+}
+
+const ServerText = styled.div`
+    display: grid;
+    padding: .2em;
+    overflow: hidden;
+    border-radius: 50%;
+    place-items: center;
+    color: var(--foreground);
+    background: var(--primary-background);
+`;
+
+const fallback = '/assets/group.png';
+export default function ServerIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>) {
+    const { client } = useContext(AppContext);
+
+    const { target, attachment, size, animate, server_name, children, as, ...svgProps } = props;
+    const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate);
+
+    if (typeof iconURL === 'undefined') {
+        const name = target?.name ?? server_name ?? '';
+
+        return (
+            <ServerText style={{ width: size, height: size }}>
+                { name.split(' ')
+                      .map(x => x[0])
+                      .filter(x => typeof x !== 'undefined') }
+            </ServerText>
+        )
+    }
+
+    return (
+        <IconBase {...svgProps}
+            width={size}
+            height={size}
+            aria-hidden="true"
+            viewBox="0 0 32 32">
+            <foreignObject x="0" y="0" width="32" height="32">
+                <img src={iconURL}
+                    onError={ e => {
+                        let el = e.currentTarget;
+                        if (el.src !== fallback) {
+                            el.src = fallback
+                        }
+                    }} />
+            </foreignObject>
+        </IconBase>
+    );
+}
diff --git a/src/components/common/UserIcon.tsx b/src/components/common/UserIcon.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..71e861ddeee95e749605660d519557929d5abbce
--- /dev/null
+++ b/src/components/common/UserIcon.tsx
@@ -0,0 +1,96 @@
+import { User } from "revolt.js";
+import { useContext } from "preact/hooks";
+import { MicOff } from "@styled-icons/feather";
+import styled, { css } from "styled-components";
+import { ThemeContext } from "../../context/Theme";
+import { Users } from "revolt.js/dist/api/objects";
+import IconBase, { IconBaseProps } from "./IconBase";
+import { AppContext } from "../../context/revoltjs/RevoltClient";
+
+type VoiceStatus = "muted";
+interface Props extends IconBaseProps<User> {
+    status?: boolean;
+    voice?: VoiceStatus;
+}
+
+export function useStatusColour(user?: User) {
+    const theme = useContext(ThemeContext);
+
+    return (
+        user?.online &&
+            user?.status?.presence !== Users.Presence.Invisible
+                ? user?.status?.presence === Users.Presence.Idle
+                    ? theme["status-away"]
+                    : user?.status?.presence ===
+                        Users.Presence.Busy
+                    ? theme["status-busy"]
+                    : theme["status-online"]
+                : theme["status-invisible"]
+    );
+}
+
+const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
+    width: 10px;
+    height: 10px;
+    border-radius: 50%;
+
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    svg {
+        stroke: white;
+    }
+
+    ${ props => props.status === 'muted' && css`
+        background: var(--error);
+    ` }
+`;
+
+const fallback = '/assets/user.png';
+export default function UserIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>) {
+    const { client } = useContext(AppContext);
+
+    const { target, attachment, size, voice, status, animate, children, as, ...svgProps } = props;
+    const iconURL = client.generateFileURL(target?.avatar ?? attachment, { max_side: 256 }, animate);
+
+    return (
+        <IconBase {...svgProps}
+            width={size}
+            height={size}
+            aria-hidden="true"
+            viewBox="0 0 32 32">
+            <foreignObject x="0" y="0" width="32" height="32">
+                {
+                    <img src={iconURL}
+                        draggable={false}
+                        onError={ e => {
+                            let el = e.currentTarget;
+                            if (el.src !== fallback) {
+                                el.src = fallback
+                            }
+                        }} />
+                }
+            </foreignObject>
+            {props.status && (
+                <circle
+                    cx="27"
+                    cy="27"
+                    r="5"
+                    fill={useStatusColour(target)}
+                />
+            )}
+            {props.voice && (
+                <foreignObject
+                    x="22"
+                    y="22"
+                    width="10"
+                    height="10">
+                    <VoiceIndicator status={props.voice}>
+                        {props.voice === "muted" && <MicOff size={6} />}
+                    </VoiceIndicator>
+                </foreignObject>
+            )}
+        </IconBase>
+    );
+}
diff --git a/src/context/Settings.tsx b/src/context/Settings.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f45005ecfceb662802f6b75ca7ab65db5ef744f8
--- /dev/null
+++ b/src/context/Settings.tsx
@@ -0,0 +1,32 @@
+// This code is more or less redundant, but settings has so little state
+// updates that I can't be asked to pass everything through props each
+// time when I can just use the Context API.
+//
+// Replace references to SettingsContext with connectState in the future
+// if it does cause problems though.
+
+import { Settings } from "../redux/reducers/settings";
+import { connectState } from "../redux/connector";
+import { Children } from "../types/Preact";
+import { createContext } from "preact";
+
+export const SettingsContext = createContext<Settings>({} as any);
+
+interface Props {
+    children?: Children,
+    settings: Settings
+}
+
+function Settings(props: Props) {
+    return (
+        <SettingsContext.Provider value={props.settings}>
+            { props.children }
+        </SettingsContext.Provider>
+    )
+}
+
+export default connectState(Settings, state => {
+    return {
+        settings: state.settings
+    }
+});
diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx
index dc919f203cf0d5ae0b632215212955ef19f23c4b..c93b95a16a41d6ff30b9fb299f337722bc1c3b3d 100644
--- a/src/context/revoltjs/RevoltClient.tsx
+++ b/src/context/revoltjs/RevoltClient.tsx
@@ -201,11 +201,12 @@ function Context({ auth, sync, children, dispatcher }: Props) {
                     }
                 }
             } else {
-                await client
-                    .fetchConfiguration()
-                    .catch(() =>
-                        console.error("Failed to connect to API server.")
-                    );
+                try {
+                    await client.fetchConfiguration()
+                } catch (err) {
+                    console.error("Failed to connect to API server.");
+                }
+
                 setStatus(ClientStatus.READY);
             }
         })();
diff --git a/src/lib/PaintCounter.tsx b/src/lib/PaintCounter.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..81251722d3889c0e66753014886693777803c62f
--- /dev/null
+++ b/src/lib/PaintCounter.tsx
@@ -0,0 +1,12 @@
+import { useState } from "preact/hooks";
+
+const counts: { [key: string]: number } = {};
+
+export default function PaintCounter() {
+    const [uniqueId] = useState('' + Math.random());
+    const count = counts[uniqueId] ?? 0;
+    counts[uniqueId] = count + 1;
+    return (
+        <span>Painted {count + 1} time(s).</span>
+    )
+}
diff --git a/src/lib/windowSize.ts b/src/lib/windowSize.ts
new file mode 100644
index 0000000000000000000000000000000000000000..55089b4e103ad3eda85ce0d2bec1493340b09382
--- /dev/null
+++ b/src/lib/windowSize.ts
@@ -0,0 +1,24 @@
+import { useEffect, useState } from "preact/hooks";
+
+export function useWindowSize() {
+    const [windowSize, setWindowSize] = useState({
+        width: window.innerWidth,
+        height: window.innerHeight
+    });
+
+    useEffect(() => {
+        function handleResize() {
+            setWindowSize({
+                width: window.innerWidth,
+                height: window.innerHeight
+            });
+        }
+
+        window.addEventListener("resize", handleResize);
+        handleResize();
+
+        return () => window.removeEventListener("resize", handleResize);
+    }, []);
+
+    return windowSize;
+}
diff --git a/src/pages/App.tsx b/src/pages/App.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6cfc6d0f0475606f6b6479792670f4de9e34914b
--- /dev/null
+++ b/src/pages/App.tsx
@@ -0,0 +1,18 @@
+import { OverlappingPanels } from "react-overlapping-panels";
+import { Switch, Route } from "react-router-dom";
+
+import Home from './home/Home';
+
+export default function App() {
+    return (
+        <OverlappingPanels
+            width="100vw"
+            height="100%">
+            <Switch>
+                <Route path="/">
+                    <Home />
+                </Route>
+            </Switch>
+        </OverlappingPanels>
+    );
+};
diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4ae75dbdf46e2655de9545e39e158ab1a515d06f
--- /dev/null
+++ b/src/pages/home/Home.tsx
@@ -0,0 +1,59 @@
+import { useChannels, useForceUpdate, useServers, useUser } from "../../context/revoltjs/hooks";
+import ChannelIcon from "../../components/common/ChannelIcon";
+import ServerIcon from "../../components/common/ServerIcon";
+import UserIcon from "../../components/common/UserIcon";
+import PaintCounter from "../../lib/PaintCounter";
+
+export function Nested() {
+    const ctx = useForceUpdate();
+
+    let user = useUser('01EX2NCWQ0CHS3QJF0FEQS1GR4', ctx)!;
+    let user2 = useUser('01EX40TVKYNV114H8Q8VWEGBWQ', ctx)!;
+    let user3 = useUser('01F5GV44HTXP3MTCD2VPV42DPE', ctx)!;
+
+    let channels = useChannels(undefined, ctx);
+    let servers = useServers(undefined, ctx);
+
+    return (
+        <>
+            <h3>Nested component</h3>
+            <PaintCounter />
+            @{ user.username } is { user.online ? 'online' : 'offline' }<br/><br/>
+
+            <h3>UserIcon Tests</h3>
+            <UserIcon size={64} target={user} />
+            <UserIcon size={64} target={user} status />
+            <UserIcon size={64} target={user} voice='muted' />
+            <UserIcon size={64} attachment={user2.avatar} />
+            <UserIcon size={64} attachment={user3.avatar} />
+            <UserIcon size={64} attachment={user3.avatar} animate />
+
+            <h3>Channels</h3>
+            { channels.map(channel =>
+                channel &&
+                channel.channel_type !== 'SavedMessages' &&
+                channel.channel_type !== 'DirectMessage' &&
+                <ChannelIcon size={48} target={channel} />
+            ) }
+
+            <h3>Servers</h3>
+            { servers.map(server =>
+                server &&
+                <ServerIcon size={48} target={server} />
+            ) }
+
+            <br/><br/>
+            <p>{ 'test long paragraph'.repeat(2000) }</p>
+        </>
+    )
+}
+
+export default function Home() {
+    return (
+        <div style={{ overflowY: 'scroll', height: '100vh' }}>
+            <h1>HOME</h1>
+            <PaintCounter />
+            <Nested />
+        </div>
+    );
+}
diff --git a/src/pages/login/Login.module.scss b/src/pages/login/Login.module.scss
index 3b982bbb3159c37ae6d97ac635a85eb5f73b1613..27bd99baab51296203fb57e70767b4d6db00329d 100644
--- a/src/pages/login/Login.module.scss
+++ b/src/pages/login/Login.module.scss
@@ -1,4 +1,7 @@
 .login {
+    width: 100%;
+    height: 100%;
+
     display: flex;
     flex-direction: row;
 
diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx
index 3eda13ca4097151c1c9f9eb93ae8e68ac6c946e2..2334a17432d0198968671c634acf9fcff6105e76 100644
--- a/src/pages/login/Login.tsx
+++ b/src/pages/login/Login.tsx
@@ -15,7 +15,7 @@ import { FormCreate } from "./forms/FormCreate";
 import { FormResend } from "./forms/FormResend";
 import { FormReset, FormSendReset } from "./forms/FormReset";
 
-export const Login = () => {
+export default function Login() {
     const theme = useContext(ThemeContext);
     const { client } = useContext(AppContext);
 
diff --git a/src/styles/_page.scss b/src/styles/_page.scss
index 210e54c3e560cb36772ff5737b768ca53cf40133..c99320ab4f4498b5001754a476e22d3913d0067c 100644
--- a/src/styles/_page.scss
+++ b/src/styles/_page.scss
@@ -8,7 +8,7 @@
 }
 
 html {
-    contain: content;
+    // contain: content;
     background: var(--background);
     background-size: cover !important;
     background-repeat: no-repeat !important;
@@ -16,6 +16,8 @@ html {
 
 html,
 body {
+    margin: 0;
+    height: 100%;
     font-family: "Open Sans", sans-serif;
     -webkit-font-smoothing: antialiased;
     -moz-osx-font-smoothing: grayscale;
@@ -29,3 +31,11 @@ body {
 
     scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
 }
+
+#app {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+}
diff --git a/vite.config.ts b/vite.config.ts
index d53d1b3c1e75f1f16b8c5ea2c2f1cbf44bd2b573..372f38d435de02caf5103f0677001cd72eca95bf 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -34,6 +34,7 @@ export default defineConfig({
     })
   ],
   build: {
+    sourcemap: true,
     rollupOptions: {
       input: {
         main: resolve(__dirname, 'index.html'),
diff --git a/yarn.lock b/yarn.lock
index dd31e17e9e926eed71efbb13814c54b27cf0598b..76c2a99285a8816571f9463a2c871f71af58859a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2969,10 +2969,10 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
-react-overlapping-panels@1.1.2-patch.0:
-  version "1.1.2-patch.0"
-  resolved "https://registry.yarnpkg.com/react-overlapping-panels/-/react-overlapping-panels-1.1.2-patch.0.tgz#335649735c029d334daea19ef6e30efc76b128fd"
-  integrity sha512-PaXxk5HxBMYg46iADGGhkgXqqweJWo7yjSeT4/o0Q3s6Q7pl7Rz23lM3oW2gdJHBDOs/zBpZ+ZIP4j6grQlCOA==
+react-overlapping-panels@1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/react-overlapping-panels/-/react-overlapping-panels-1.2.1.tgz#3775a09ae6c83604d058d4082d1c8fed5cc59fe9"
+  integrity sha512-vkHLqX+X6HO13nAppZ5Z4tt4s8IMTA8sVf/FZFnnoqlQFIfTJAgdgZDa3LejMIrOJO6YMftVSVpzmusWTxvlUA==
 
 react-redux@^7.2.4:
   version "7.2.4"