diff --git a/package.json b/package.json
index 7ae716823925338d25045fbf86cfb55b4dee7573..1048e99af21ec4c6887ecefd858b1bd91310a10d 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
     "@types/styled-components": "^5.1.10",
     "@typescript-eslint/eslint-plugin": "^4.27.0",
     "@typescript-eslint/parser": "^4.27.0",
+    "classnames": "^2.3.1",
     "dayjs": "^1.10.5",
     "detect-browser": "^5.2.0",
     "eslint": "^7.28.0",
diff --git a/src/components/common/ChannelIcon.tsx b/src/components/common/ChannelIcon.tsx
index 8be9fe8085e234444d9c20ddf99116f83029176a..af245c3f90d73235bb41a8fcdf91ec68ac9a0401 100644
--- a/src/components/common/ChannelIcon.tsx
+++ b/src/components/common/ChannelIcon.tsx
@@ -1,7 +1,7 @@
 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 { ImageIconBase, IconBaseProps } from "./IconBase";
 import { AppContext } from "../../context/revoltjs/RevoltClient";
 
 interface Props extends IconBaseProps<Channels.GroupChannel | Channels.TextChannel> {
@@ -9,10 +9,10 @@ interface Props extends IconBaseProps<Channels.GroupChannel | Channels.TextChann
 }
 
 const fallback = '/assets/group.png';
-export default function ChannelIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>) {
+export default function ChannelIcon(props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>) {
     const { client } = useContext(AppContext);
 
-    const { size, target, attachment, isServerChannel: server, animate, children, as, ...svgProps } = props;
+    const { size, target, attachment, isServerChannel: server, animate, children, as, ...imgProps } = props;
     const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate);
     const isServerChannel = server || target?.channel_type === 'TextChannel';
 
@@ -25,21 +25,18 @@ export default function ChannelIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVG
     }
 
     return (
-        <IconBase {...svgProps}
+        // ! fixme: replace fallback with <picture /> + <source />
+        <ImageIconBase {...imgProps}
             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>
+            square={isServerChannel}
+            src={iconURL ?? fallback}
+            onError={ e => {
+                let el = e.currentTarget;
+                if (el.src !== fallback) {
+                    el.src = fallback
+                }
+            }} />
     );
 }
diff --git a/src/components/common/IconBase.tsx b/src/components/common/IconBase.tsx
index fb1f75bdb6bf450576ee0d7e4c5c60318a5e6c16..bebc0a8f8b23c57fa6d829af27c29c2358122207 100644
--- a/src/components/common/IconBase.tsx
+++ b/src/components/common/IconBase.tsx
@@ -9,7 +9,11 @@ export interface IconBaseProps<T> {
     animate?: boolean;
 }
 
-export default styled.svg<{ square?: boolean }>`
+interface IconModifiers {
+    square?: boolean
+}
+
+export default styled.svg<IconModifiers>`
     img {
         width: 100%;
         height: 100%;
@@ -20,3 +24,11 @@ export default styled.svg<{ square?: boolean }>`
         ` }
     }
 `;
+
+export const ImageIconBase = styled.img<IconModifiers>`
+    object-fit: cover;
+
+    ${ props => !props.square && css`
+        border-radius: 50%;
+    ` }
+`;
diff --git a/src/components/common/ServerIcon.tsx b/src/components/common/ServerIcon.tsx
index 1cf0297f445bb440d6a4ea14c4902f046e9e3002..b9ea81182fbd7ffe0270e1e550908cff2e5e9f37 100644
--- a/src/components/common/ServerIcon.tsx
+++ b/src/components/common/ServerIcon.tsx
@@ -1,7 +1,7 @@
 import styled from "styled-components";
 import { useContext } from "preact/hooks";
 import { Server } from "revolt.js/dist/api/objects";
-import IconBase, { IconBaseProps } from "./IconBase";
+import { IconBaseProps, ImageIconBase } from "./IconBase";
 import { AppContext } from "../../context/revoltjs/RevoltClient";
 
 interface Props extends IconBaseProps<Server> {
@@ -19,10 +19,10 @@ const ServerText = styled.div`
 `;
 
 const fallback = '/assets/group.png';
-export default function ServerIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>) {
+export default function ServerIcon(props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>) {
     const { client } = useContext(AppContext);
 
-    const { target, attachment, size, animate, server_name, children, as, ...svgProps } = props;
+    const { target, attachment, size, animate, server_name, children, as, ...imgProps } = props;
     const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate);
 
     if (typeof iconURL === 'undefined') {
@@ -38,20 +38,16 @@ export default function ServerIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVGE
     }
 
     return (
-        <IconBase {...svgProps}
+        <ImageIconBase {...imgProps}
             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>
+            src={iconURL}
+            onError={ e => {
+                let el = e.currentTarget;
+                if (el.src !== fallback) {
+                    el.src = fallback
+                }
+            }} />
     );
 }
diff --git a/src/components/common/UserHeader.tsx b/src/components/common/UserHeader.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..eae71db9a3ae59d0190261af7c3b539a00b57f8f
--- /dev/null
+++ b/src/components/common/UserHeader.tsx
@@ -0,0 +1,79 @@
+import { User } from "revolt.js";
+import Header from "../ui/Header";
+import UserIcon from "./UserIcon";
+import UserStatus from './UserStatus';
+import styled from "styled-components";
+import { Localizer } from 'preact-i18n';
+import { Settings } from "@styled-icons/feather";
+import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
+
+const HeaderBase = styled.div`
+    gap: 0;
+    flex-grow: 1;
+    min-width: 0;
+    display: flex;
+    flex-direction: column;
+    
+    * {
+        min-width: 0;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+    }
+
+    .username {
+        cursor: pointer;
+        font-size: 16px;
+        font-weight: 600;
+    }
+
+    .status {
+        cursor: pointer;
+        font-size: 12px;
+        margin-top: -2px;
+    }
+`;
+
+interface Props {
+    user: User
+}
+
+export default function UserHeader({ user }: Props) {
+    function openPresenceSelector() {
+        // openContextMenu("Status");
+    }
+
+    function writeClipboard(a: string) {
+        alert('unimplemented');
+    }
+
+    return (
+        <Header placement="secondary">
+            <UserIcon
+                target={user}
+                size={32}
+                status
+                onClick={openPresenceSelector}
+            />
+            <HeaderBase>
+                <Localizer>
+                    {/*<Tooltip content={<Text id="app.special.copy_username" />}>*/}
+                        <span className="username"
+                            onClick={() => writeClipboard(user.username)}>
+                            @{user.username}
+                        </span>
+                    {/*</Tooltip>*/}
+                </Localizer>
+                <span className="status"
+                    onClick={openPresenceSelector}>
+                    <UserStatus user={user} />
+                </span>
+            </HeaderBase>
+            { !isTouchscreenDevice && <div className="actions">
+                {/*<IconButton to="/settings">*/}
+                    <Settings size={24} />
+                {/*</IconButton>*/}
+            </div> }
+        </Header>
+    )
+}
diff --git a/src/components/common/UserIcon.tsx b/src/components/common/UserIcon.tsx
index 71e861ddeee95e749605660d519557929d5abbce..124d75719ba3806bf54f0e8f416775bd2ad326df 100644
--- a/src/components/common/UserIcon.tsx
+++ b/src/components/common/UserIcon.tsx
@@ -52,7 +52,8 @@ export default function UserIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVGEle
     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);
+    const iconURL = client.generateFileURL(target?.avatar ?? attachment, { max_side: 256 }, animate)
+        ?? client.users.getDefaultAvatarURL(target!._id);
 
     return (
         <IconBase {...svgProps}
diff --git a/src/components/common/UserStatus.tsx b/src/components/common/UserStatus.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5a525517ecbc4878d132c5c93e02143537639abe
--- /dev/null
+++ b/src/components/common/UserStatus.tsx
@@ -0,0 +1,31 @@
+import { User } from "revolt.js";
+import { Text } from "preact-i18n";
+import { Users } from "revolt.js/dist/api/objects";
+
+interface Props {
+    user: User;
+}
+
+export default function UserStatus({ user }: Props) {
+    if (user.online) {
+        if (user.status?.text) {
+            return <>{user.status?.text}</>;
+        }
+
+        if (user.status?.presence === Users.Presence.Busy) {
+            return <Text id="app.status.busy" />;
+        }
+
+        if (user.status?.presence === Users.Presence.Idle) {
+            return <Text id="app.status.idle" />;
+        }
+
+        if (user.status?.presence === Users.Presence.Invisible) {
+            return <Text id="app.status.offline" />;
+        }
+
+        return <Text id="app.status.online" />;
+    }
+
+    return <Text id="app.status.offline" />;
+}
diff --git a/src/components/navigation/LeftSidebar.tsx b/src/components/navigation/LeftSidebar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6852a848c9b506463a1320456dcd88a94f02de4f
--- /dev/null
+++ b/src/components/navigation/LeftSidebar.tsx
@@ -0,0 +1,32 @@
+import { Route, Switch } from "react-router";
+import SidebarBase from "./SidebarBase";
+
+import ServerListSidebar from "./left/ServerListSidebar";
+import ServerSidebar from "./left/ServerSidebar";
+import HomeSidebar from "./left/HomeSidebar";
+
+export default function LeftSidebar() {
+    return (
+        <SidebarBase>
+            <Switch>
+                <Route path="/settings" />
+                <Route path="/server/:server/channel/:channel">
+                    <ServerListSidebar />
+                    <ServerSidebar />
+                </Route>
+                <Route path="/server/:server">
+                    <ServerListSidebar />
+                    <ServerSidebar />
+                </Route>
+                <Route path="/channel/:channel">
+                    <ServerListSidebar />
+                    <HomeSidebar />
+                </Route>
+                <Route path="/">
+                    <ServerListSidebar />
+                    <HomeSidebar />
+                </Route>
+            </Switch>
+        </SidebarBase>
+    );
+};
diff --git a/src/components/navigation/RightSidebar.tsx b/src/components/navigation/RightSidebar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..775f58340938c53d1a0af9c583d80ff888ae8c0e
--- /dev/null
+++ b/src/components/navigation/RightSidebar.tsx
@@ -0,0 +1,20 @@
+import { Route, Switch } from "react-router";
+import SidebarBase from "./SidebarBase";
+
+// import { MemberSidebar } from "./right/MemberSidebar";
+
+export default function RightSidebar() {
+    return (
+        <SidebarBase>
+            <Switch>
+                {/* 
+                <Route path="/server/:server/channel/:channel">
+                    <MemberSidebar />
+                </Route>
+                <Route path="/channel/:channel">
+                    <MemberSidebar />
+                </Route> */ }
+            </Switch>
+        </SidebarBase>
+    );
+};
diff --git a/src/components/navigation/SidebarBase.tsx b/src/components/navigation/SidebarBase.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..677c9145b229363327f68344e3e62dcf34d97e41
--- /dev/null
+++ b/src/components/navigation/SidebarBase.tsx
@@ -0,0 +1,8 @@
+import styled from "styled-components";
+
+export default styled.div`
+    height: 100%;
+    display: flex;
+    user-select: none;
+    flex-direction: row;
+`;
diff --git a/src/components/navigation/items/ButtonItem.tsx b/src/components/navigation/items/ButtonItem.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0acfbf02d2638fb2a5cffd32b044b3c5c21a9571
--- /dev/null
+++ b/src/components/navigation/items/ButtonItem.tsx
@@ -0,0 +1,157 @@
+import classNames from 'classnames';
+import styles from "./Item.module.scss";
+import UserIcon from '../../common/UserIcon';
+import { Localizer, Text } from "preact-i18n";
+import { X, Zap } from "@styled-icons/feather";
+import UserStatus from '../../common/UserStatus';
+import { Children } from "../../../types/Preact";
+import ChannelIcon from '../../common/ChannelIcon';
+import { Channels, Users } from "revolt.js/dist/api/objects";
+import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
+
+interface CommonProps {
+    active?: boolean
+    alert?: 'unread' | 'mention'
+    alertCount?: number
+}
+
+type UserProps = CommonProps & {
+    user: Users.User,
+    context?: Channels.Channel,
+    channel?: Channels.DirectMessageChannel
+}
+
+export function UserButton({ active, alert, alertCount, user, context, channel }: UserProps) {
+    // const { openScreen } = useContext(IntermediateContext);
+
+    return (
+        <div
+            className={classNames(styles.item, styles.user)}
+            data-active={active}
+            data-alert={typeof alert === 'string'}
+            data-online={typeof channel !== 'undefined' || (user.online && user.status?.presence !== Users.Presence.Invisible)}
+            /*onContextMenu={attachContextMenu('Menu', {
+                user: user._id,
+                channel: channel?._id,
+                unread: alert,
+                contextualChannel: context?._id
+            })}*/
+        >
+            <div className={styles.avatar}>
+                <UserIcon target={user} size={32} status />
+            </div>
+            <div className={styles.name}>
+                <div>{user.username}</div>
+                {
+                    <div className={styles.subText}>
+                        { channel?.last_message && alert ? (
+                            channel.last_message.short
+                        ) : (
+                            <UserStatus user={user} />
+                        ) }
+                    </div>
+                }
+            </div>
+            <div className={styles.button}>
+                { context?.channel_type === "Group" &&
+                    context.owner === user._id && (
+                        <Localizer>
+                            {/*<Tooltip
+                                content={
+                                    <Text id="app.main.groups.owner" />
+                                }
+                            >*/}
+                                <Zap size={20} />
+                            {/*</Tooltip>*/}
+                        </Localizer>
+                )}
+                {alert && <div className={styles.alert} data-style={alert}>{ alertCount }</div>}
+                { !isTouchscreenDevice && channel &&
+                    /*<IconButton
+                        className={styles.icon}
+                        style="default"
+                        onClick={() => openScreen({ id: 'special_prompt', type: 'close_dm', target: channel })}
+                    >*/
+                        <X size={24} />
+                    /*</IconButton>*/
+                }
+            </div>
+        </div>
+    )
+}
+
+type ChannelProps = CommonProps & {
+    channel: Channels.Channel,
+    user?: Users.User
+    compact?: boolean
+}
+
+export function ChannelButton({ active, alert, alertCount, channel, user, compact }: ChannelProps) {
+    if (channel.channel_type === 'SavedMessages') throw "Invalid channel type.";
+    if (channel.channel_type === 'DirectMessage') {
+        if (typeof user === 'undefined') throw "No user provided.";
+        return <UserButton {...{ active, alert, channel, user }} />
+    }
+
+    //const { openScreen } = useContext(IntermediateContext);
+
+    return (
+        <div
+            data-active={active}
+            data-alert={typeof alert === 'string'}
+            className={classNames(styles.item, { [styles.compact]: compact })}
+            //onContextMenu={attachContextMenu('Menu', { channel: channel._id })}>
+            >
+            <div className={styles.avatar}>
+                <ChannelIcon target={channel} size={compact ? 24 : 32} />
+            </div>
+            <div className={styles.name}>
+                <div>{channel.name}</div>
+                { channel.channel_type === 'Group' &&
+                    <div className={styles.subText}>
+                        {(channel.last_message && alert) ? (
+                            channel.last_message.short
+                        ) : (
+                            <Text
+                                id="quantities.members"
+                                plural={channel.recipients.length}
+                                fields={{ count: channel.recipients.length }}
+                            />
+                        )}
+                    </div>
+                }
+            </div>
+            <div className={styles.button}>
+                {alert && <div className={styles.alert} data-style={alert}>{ alertCount }</div>}
+                {!isTouchscreenDevice && channel.channel_type === "Group" && (
+                    /*<IconButton
+                        className={styles.icon}
+                        style="default"
+                        onClick={() => openScreen({ id: 'special_prompt', type: 'leave_group', target: channel })}
+                    >*/
+                        <X size={24} />
+                    /*</IconButton>*/
+                )}
+            </div>
+        </div>
+    )
+}
+
+type ButtonProps = CommonProps & {
+    onClick?: () => void
+    children?: Children
+    className?: string
+    compact?: boolean
+}
+
+export default function ButtonItem({ active, alert, alertCount, onClick, className, children, compact }: ButtonProps) {
+    return (
+        <div className={classNames(styles.item, { [styles.compact]: compact, [styles.normal]: !compact }, className)}
+            onClick={onClick}
+            data-active={active}
+            data-alert={typeof alert === 'string'}>
+            <div className={styles.content}>{ children }</div>
+                {alert && <div className={styles.alert} data-style={alert}>{ alertCount }</div>}
+        </div>
+    )
+}
diff --git a/src/components/navigation/items/ConnectionStatus.tsx b/src/components/navigation/items/ConnectionStatus.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..88a886c7b373b3907291f9457299cc92d506d90f
--- /dev/null
+++ b/src/components/navigation/items/ConnectionStatus.tsx
@@ -0,0 +1,35 @@
+import { Text } from "preact-i18n";
+import Banner from "../../ui/Banner";
+import { useContext } from "preact/hooks";
+import { AppContext, ClientStatus } from "../../../context/revoltjs/RevoltClient";
+
+export default function ConnectionStatus() {
+    const { status } = useContext(AppContext);
+
+    if (status === ClientStatus.OFFLINE) {
+        return (
+            <Banner>
+                <Text id="app.special.status.offline" />
+            </Banner>
+        );
+    } else if (status === ClientStatus.DISCONNECTED) {
+        return (
+            <Banner>
+                <Text id="app.special.status.disconnected" />
+            </Banner>
+        );
+    } else if (status === ClientStatus.CONNECTING) {
+        return (
+            <Banner>
+                <Text id="app.special.status.connecting" />
+            </Banner>
+        );
+    } else if (status === ClientStatus.RECONNECTING) {
+        return (
+            <Banner>
+                <Text id="app.special.status.reconnecting" />
+            </Banner>
+        );
+    }
+    return null;
+}
diff --git a/src/components/navigation/items/Item.module.scss b/src/components/navigation/items/Item.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..d06e98f4909055e2f217a05fc30b8ea3edf87093
--- /dev/null
+++ b/src/components/navigation/items/Item.module.scss
@@ -0,0 +1,303 @@
+.item {
+    height: 48px;
+    display: flex;
+    padding: 0 8px;
+    user-select: none;
+    border-radius: 6px;
+    margin-bottom: 2px;
+
+    gap: 8px;
+    align-items: center;
+    flex-direction: row;
+    
+    cursor: pointer;
+    font-size: 16px;
+    transition: .1s ease-in-out background-color;
+
+    color: var(--tertiary-foreground);
+    stroke: var(--tertiary-foreground);
+
+    &.normal {
+        height: 38px;
+    }
+
+    &.compact {
+        height: 32px;
+    }
+
+    &.user {
+        opacity: 0.4;
+        cursor: pointer;
+        transition: .15s ease opacity;
+
+        &[data-online="true"],
+        &:hover {
+            opacity: 1;
+        }
+    }
+
+    &:hover {
+        background: var(--hover);
+    }
+
+    div {
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        transition: color .1s ease-in-out;
+
+        &.content {
+            gap: 8px;
+            flex-grow: 1;
+            min-width: 0;
+            display: flex;
+            align-items: center;
+            flex-direction: row;
+
+            svg {
+                flex-shrink: 0;
+            }
+
+            span {
+                overflow: hidden;
+                white-space: nowrap;
+                text-overflow: ellipsis;
+            }
+        }
+
+        &.avatar {
+            flex-shrink: 0;
+        }
+
+        &.name {
+            flex-grow: 1;
+            display: flex;
+            font-weight: 600;
+            font-size: .90625rem;
+            flex-direction: column;
+
+            .subText {
+                margin-top: -1px;
+                font-weight: 500;
+                font-size: .6875rem;
+                color: var(--tertiary-foreground);
+            }
+        }
+
+        &.button {
+            flex-shrink: 0;
+
+            svg {
+                opacity: 0;
+                display: none;
+                transition: .1s ease-in-out opacity;
+            }
+        }
+    }
+
+    &:not(.compact):hover {
+        div.button .alert {
+            display: none;
+        }
+
+        div.button svg {
+            opacity: 1;
+            display: block;
+        }
+    }
+
+    &[data-active="true"] {
+        cursor: default;
+        background: var(--hover);
+
+        .unread {
+            display: none;
+        }
+    }
+
+    &[data-alert="true"], &[data-active="true"], &:hover {
+        color: var(--foreground);
+        stroke: var(--foreground);
+
+        .subText {
+            color: var(--secondary-foreground) !important;
+        }
+    }
+}
+
+.alert {
+    width: 6px;
+    height: 6px;
+    margin: 9px;
+    flex-shrink: 0;
+    border-radius: 50%;
+    background: var(--foreground);
+
+    display: grid;
+    font-size: 10px;
+    font-weight: 600;
+    place-items: center;
+
+    &[data-style="mention"] {
+        width: 16px;
+        height: 16px;
+        color: white;
+        background: var(--error);
+    }
+}
+
+/* ! FIXME: check if anything is missing, then remove this block
+.olditem {
+    display: flex;
+    user-select: none;
+    align-items: center;
+    flex-direction: row;
+
+    gap: 8px;
+    height: 48px;
+    padding: 0 8px;
+    cursor: pointer;
+    font-size: 16px;
+    border-radius: 6px;
+    box-sizing: content-box;
+    transition: .1s ease background-color;
+
+    color: var(--tertiary-foreground);
+    stroke: var(--tertiary-foreground);
+
+    .avatar {
+        flex-shrink: 0;
+        height: 32px;
+        flex-shrink: 0;
+        padding: 10px 0;
+        box-sizing: content-box;
+
+        img {
+            width: 32px;
+            height: 32px;
+            border-radius: 50%;
+        }
+    }
+
+    div {
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        transition: color .1s ease-in-out;
+
+        &.content {
+            gap: 8px;
+            flex-grow: 1;
+            min-width: 0;
+            display: flex;
+            align-items: center;
+            flex-direction: row;
+
+            svg {
+                flex-shrink: 0;
+            }
+
+            span {
+                overflow: hidden;
+                white-space: nowrap;
+                text-overflow: ellipsis;
+            }
+        }
+
+        &.name {
+            flex-grow: 1;
+            display: flex;
+            flex-direction: column;
+            font-size: .90625rem;
+            font-weight: 600;
+
+            .subText {
+                font-size: .6875rem;
+                margin-top: -1px;
+                color: var(--tertiary-foreground);
+                font-weight: 500;
+            }
+        }
+
+        &.unread {
+            width: 6px;
+            height: 6px;
+            margin: 9px;
+            flex-shrink: 0;
+            border-radius: 50%;
+            background: var(--foreground);
+        }
+
+        &.button {
+            flex-shrink: 0;
+
+            .icon {
+                opacity: 0;
+                display: none;
+                transition: 0.1s ease opacity;
+            }
+        }
+    }
+
+    &[data-active="true"] {
+        color: var(--foreground);
+        stroke: var(--foreground);
+        background: var(--hover);
+        cursor: default;
+
+        .subText {
+            color: var(--secondary-foreground) !important;
+        }
+
+        .unread {
+            display: none;
+        }
+    }
+
+    &[data-alert="true"] {
+        color: var(--secondary-foreground);
+    }
+
+    &[data-type="user"] {
+        opacity: 0.4;
+        color: var(--foreground);
+        transition: 0.15s ease opacity;
+        cursor: pointer;
+
+        &[data-online="true"],
+        &:hover {
+            opacity: 1;
+            //background: none;
+        }
+    }
+
+    &[data-size="compact"] {
+        margin-bottom: 2px;
+        height: 32px;
+        transition: border-inline-start .1s ease-in-out;
+        border-inline-start: 4px solid transparent;
+
+        &[data-active="true"] {
+            border-inline-start: 4px solid var(--accent);
+            border-radius: 4px;
+        }
+    }
+
+    &[data-size="small"] {
+        margin-bottom: 2px;
+        height: 42px;
+    }
+
+    &:hover {
+        background: var(--hover);
+
+        div.button .unread {
+            display: none;
+        }
+
+        div.button .icon {
+            opacity: 1;
+            display: block;
+        }
+    }
+}*/
diff --git a/src/components/navigation/left/HomeSidebar.tsx b/src/components/navigation/left/HomeSidebar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4e767e3a82476def36e8330c359efa8accdcfd15
--- /dev/null
+++ b/src/components/navigation/left/HomeSidebar.tsx
@@ -0,0 +1,160 @@
+import { Localizer, Text } from "preact-i18n";
+import { useContext, useLayoutEffect } from "preact/hooks";
+import { Home, Users, Tool, Settings, Save } from "@styled-icons/feather";
+
+import { Link, Redirect, useHistory, useLocation, useParams } from "react-router-dom";
+import { WithDispatcher } from "../../../redux/reducers";
+import { Unreads } from "../../../redux/reducers/unreads";
+import { connectState } from "../../../redux/connector";
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
+import { useChannels, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
+import { User } from "revolt.js";
+import { Users as UsersNS } from 'revolt.js/dist/api/objects';
+import { mapChannelWithUnread, useUnreads } from "./common";
+import { Channels } from "revolt.js/dist/api/objects";
+import UserIcon from '../../common/UserIcon';
+import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
+import ConnectionStatus from '../items/ConnectionStatus';
+import UserStatus from '../../common/UserStatus';
+import ButtonItem, { ChannelButton } from '../items/ButtonItem';
+import styled from "styled-components";
+import Header from '../../ui/Header';
+import UserHeader from "../../common/UserHeader";
+import Category from '../../ui/Category';
+import PaintCounter from "../../../lib/PaintCounter";
+
+type Props = WithDispatcher & {
+    unreads: Unreads;
+}
+
+const HomeBase = styled.div`
+    height: 100%;
+    width: 240px;
+    display: flex;
+    flex-shrink: 0;
+    flex-direction: column;
+    background: var(--secondary-background);
+`;
+
+const HomeList = styled.div`
+    padding: 6px;
+    flex-grow: 1;
+    overflow-y: scroll;
+
+    > svg {
+        width: 100%;
+    }
+`;
+
+function HomeSidebar(props: Props) {
+    const { pathname } = useLocation();
+    const { client } = useContext(AppContext);
+    const { channel } = useParams<{ channel: string }>();
+    // const { openScreen, writeClipboard } = useContext(IntermediateContext);
+
+    const ctx = useForceUpdate();
+    const users = useUsers(undefined, ctx);
+    const channels = useChannels(undefined, ctx);
+
+    const obj = channels.find(x => x?._id === channel);
+    if (channel && !obj) return <Redirect to="/" />;
+    if (obj) useUnreads({ ...props, channel: obj });
+
+    const channelsArr = (channels
+        .filter(
+            x => x && (x.channel_type === "Group" || (x.channel_type === 'DirectMessage' && x.active))
+        ) as (Channels.GroupChannel | Channels.DirectMessageChannel)[])
+        .map(x => mapChannelWithUnread(x, props.unreads));
+
+    channelsArr.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
+
+    return (
+        <HomeBase>
+            <UserHeader user={client.user!} />
+            <ConnectionStatus />
+            <HomeList>
+                {!isTouchscreenDevice && (
+                    <>
+                        <Link to="/">
+                            <ButtonItem active={pathname === "/"}>
+                                <Home size={20} />
+                                <span><Text id="app.navigation.tabs.home" /></span>
+                            </ButtonItem>
+                        </Link>
+                        <Link to="/friends">
+                            <ButtonItem
+                                active={pathname === "/friends"}
+                                alert={
+                                    typeof users.find(
+                                        user =>
+                                            user?.relationship ===
+                                            UsersNS.Relationship.Incoming
+                                    ) !== "undefined" ? 'unread' : undefined
+                                }
+                            >
+                                <Users size={20} />
+                                <span><Text id="app.navigation.tabs.friends" /></span>
+                            </ButtonItem>
+                        </Link>
+                    </>
+                )}
+                <Link to="/open/saved">
+                    <ButtonItem active={obj?.channel_type === "SavedMessages"}>
+                        <Save size={20} />
+                        <span><Text id="app.navigation.tabs.saved" /></span>
+                    </ButtonItem>
+                </Link>
+                {import.meta.env.DEV && (
+                    <Link to="/dev">
+                        <ButtonItem active={pathname === "/dev"}>
+                            <Tool size={20} />
+                            <span><Text id="app.navigation.tabs.dev" /></span>
+                        </ButtonItem>
+                    </Link>
+                )}
+                <Localizer>
+                    <Category
+                        text={
+                            (
+                                <Text id="app.main.categories.conversations" />
+                            ) as any
+                        }
+                        action={() => /*openScreen({ id: "special_input", type: "create_group" })*/{}}
+                    />
+                </Localizer>
+                {channelsArr.length === 0 && <img src="/assets/images/placeholder.svg" />}
+                {channelsArr.map(x => {
+                    let user;
+                    if (x.channel_type === 'DirectMessage') {
+                        let recipient = client.channels.getRecipient(x._id);
+                        user = users.find(x => x!._id === recipient);
+                    }
+                    
+                    return (
+                        <Link to={`/channel/${x._id}`}>
+                            <ChannelButton
+                                user={user}
+                                channel={x}
+                                alert={x.unread}
+                                alertCount={x.alertCount}
+                                active={x._id === channel}
+                            />
+                        </Link>
+                    );
+                })}
+                <PaintCounter />
+            </HomeList>
+        </HomeBase>
+    );
+};
+
+export default connectState(
+    HomeSidebar,
+    state => {
+        return {
+            unreads: state.unreads
+        };
+    },
+    true,
+    true
+);
diff --git a/src/components/navigation/left/ServerListSidebar.tsx b/src/components/navigation/left/ServerListSidebar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7ff3e74ddd35f4a93f6c87ddb412e2494f243358
--- /dev/null
+++ b/src/components/navigation/left/ServerListSidebar.tsx
@@ -0,0 +1,215 @@
+import { useContext } from "preact/hooks";
+import { PlusCircle } from "@styled-icons/feather";
+import { Channel, Servers } from "revolt.js/dist/api/objects";
+import { Link, useLocation, useParams } from "react-router-dom";
+import { useChannels, useForceUpdate, useServers } from "../../../context/revoltjs/hooks";
+import { mapChannelWithUnread } from "./common";
+import { Unreads } from "../../../redux/reducers/unreads";
+import { connectState } from "../../../redux/connector";
+import styled, { css } from "styled-components";
+import { Children } from "../../../types/Preact";
+import LineDivider from "../../ui/LineDivider";
+import ServerIcon from "../../common/ServerIcon";
+import PaintCounter from "../../../lib/PaintCounter";
+
+function Icon({ children, unread, size }: { children: Children, unread?: 'mention' | 'unread', size: number }) {
+    return (
+        <svg
+            width={size}
+            height={size}
+            aria-hidden="true"
+            viewBox="0 0 32 32"
+        >
+            <foreignObject x="0" y="0" width="32" height="32">
+                { children }
+            </foreignObject>
+            {unread === 'unread' && (
+                <circle
+                    cx="27"
+                    cy="27"
+                    r="5"
+                    fill={"white"}
+                />
+            )}
+            {unread === 'mention' && (
+                <circle
+                    cx="27"
+                    cy="27"
+                    r="5"
+                    fill={"red"}
+                />
+            )}
+        </svg>
+    )
+}
+
+const ServersBase = styled.div`
+    width: 52px;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+`;
+
+const ServerList = styled.div`
+    flex-grow: 1;
+    display: flex;
+    overflow-y: scroll;
+    padding-bottom: 48px;
+    flex-direction: column;
+    border-inline-end: 2px solid var(--sidebar-active);
+
+    scrollbar-width: none;
+
+    > :first-child > svg {
+        margin: 6px 0 6px 4px;
+    }
+
+    &::-webkit-scrollbar {
+        width: 0px;
+    }
+`;
+
+const ServerEntry = styled.div<{ active: boolean, invert?: boolean }>`
+    height: 44px;
+    padding: 4px;
+    margin: 2px 0 2px 4px;
+
+    border-top-left-radius: 4px;
+    border-bottom-left-radius: 4px;
+
+    img {
+        width: 32px;
+        height: 32px;
+    }
+
+    ${ props => props.active && css`
+        background: var(--sidebar-active);
+    ` }
+
+    ${ props => props.active && props.invert && css`
+        img {
+            filter: saturate(0) brightness(10);
+        }
+    ` }
+`;
+
+interface Props {
+    unreads: Unreads;
+}
+
+export function ServerListSidebar({ unreads }: Props) {
+    const ctx = useForceUpdate();
+    const activeServers = useServers(undefined, ctx) as Servers.Server[];
+    const channels = (useChannels(undefined, ctx) as Channel[])
+        .map(x => mapChannelWithUnread(x, unreads));
+    
+    const unreadChannels = channels.filter(x => x.unread)
+        .map(x => x._id);
+
+    const servers = activeServers.map(server => {
+        let alertCount = 0;
+        for (let id of server.channels) {
+            let channel = channels.find(x => x._id === id);
+            if (channel?.alertCount) {
+                alertCount += channel.alertCount;
+            }
+        }
+
+        return {
+            ...server,
+            unread: (typeof server.channels.find(x => unreadChannels.includes(x)) !== 'undefined' ?
+                ( alertCount > 0 ? 'mention' : 'unread' ) : undefined) as 'mention' | 'unread' | undefined,
+            alertCount
+        }
+    });
+
+    const path = useLocation().pathname;
+    const { server: server_id } = useParams<{ server?: string }>();
+    const server = servers.find(x => x!._id == server_id);
+
+    // const { openScreen } = useContext(IntermediateContext);
+
+    let homeUnread: 'mention' | 'unread' | undefined;
+    let alertCount = 0;
+    for (let x of channels) {
+        if (((x.channel_type === 'DirectMessage' && x.active) || x.channel_type === 'Group') && x.unread) {
+            homeUnread = 'unread';
+            alertCount += x.alertCount ?? 0;
+        }
+    }
+
+    if (alertCount > 0) homeUnread = 'mention';
+
+    return (
+        <ServersBase>
+            <ServerList>
+                <Link to={`/`}>
+                    <ServerEntry invert
+                        active={typeof server === 'undefined' && !path.startsWith('/invite')}>
+                        <Icon size={36} unread={homeUnread}>
+                            <img src="/assets/app_icon.png"  />
+                        </Icon>
+                    </ServerEntry>
+                </Link>
+                <LineDivider />
+                {
+                    servers.map(entry =>
+                        <Link to={`/server/${entry!._id}`}>
+                            <ServerEntry
+                                active={entry!._id === server?._id}
+                                //onContextMenu={attachContextMenu('Menu', { server: entry!._id })}>
+                                >
+                                <Icon size={36} unread={entry.unread}>
+                                    <ServerIcon size={32} target={entry} />
+                                </Icon>
+                            </ServerEntry>
+                        </Link>
+                    )
+                }
+                <PaintCounter small />
+            </ServerList>
+        </ServersBase>
+        /*<div className={styles.servers}>
+            <div className={styles.list}>
+                <Link to={`/`}>
+                    <div className={styles.entry}
+                        data-active={typeof server === 'undefined' && !path.startsWith('/invite')}>
+                        <Icon size={36} unread={homeUnread} alertCount={alertCount}>
+                            <div className={styles.logo} />
+                        </Icon>
+                    </div>
+                </Link>
+                <LineDivider className={styles.divider} />
+                {
+                    servers.map(entry =>
+                        <Link to={`/server/${entry!._id}`}>
+                            <div className={styles.entry}
+                                data-active={entry!._id === server?._id}
+                                onContextMenu={attachContextMenu('Menu', { server: entry!._id })}>
+                                <Icon size={36} unread={entry.unread}>
+                                    <ServerIcon id={entry!._id} size={32} />
+                                </Icon>
+                            </div>
+                        </Link>
+                    )
+                }
+            </div>
+            <div className={styles.overlay}>
+                <div className={styles.actions}>
+                    <IconButton onClick={() => openScreen({ id: 'special_input', type: 'create_server' })}>
+                        <PlusCircle size={36} />
+                    </IconButton>
+                </div>
+            </div>
+            </div> */
+    )
+}
+
+export default connectState(
+    ServerListSidebar,
+    state => {
+        return {
+            unreads: state.unreads
+        };
+    }
+);
diff --git a/src/components/navigation/left/ServerSidebar.tsx b/src/components/navigation/left/ServerSidebar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e5008550d55a40c775a39cce174514bd901dd77c
--- /dev/null
+++ b/src/components/navigation/left/ServerSidebar.tsx
@@ -0,0 +1,78 @@
+import { Link } from "react-router-dom";
+import { Settings } from "@styled-icons/feather";
+import { Redirect, useParams } from "react-router";
+import { ChannelButton } from "../items/ButtonItem";
+import { Channels } from "revolt.js/dist/api/objects";
+import { ServerPermission } from "revolt.js/dist/api/permissions";
+import { Unreads } from "../../../redux/reducers/unreads";
+import { WithDispatcher } from "../../../redux/reducers";
+import { useChannels, useForceUpdate, useServer, useServerPermission } from "../../../context/revoltjs/hooks";
+import { mapChannelWithUnread, useUnreads } from "./common";
+import Header from '../../ui/Header';
+import ConnectionStatus from '../items/ConnectionStatus';
+import { connectState } from "../../../redux/connector";
+import PaintCounter from "../../../lib/PaintCounter";
+
+interface Props {
+    unreads: Unreads;
+}
+
+function ServerSidebar(props: Props & WithDispatcher) {
+    const { server: server_id, channel: channel_id } = useParams<{ server?: string, channel?: string }>();
+    const ctx = useForceUpdate();
+
+    const server = useServer(server_id, ctx);
+    if (!server) return <Redirect to="/" />;
+
+    const permissions = useServerPermission(server._id, ctx);
+    const channels = (useChannels(server.channels, ctx)
+        .filter(entry => typeof entry !== 'undefined') as Readonly<Channels.TextChannel>[])
+        .map(x => mapChannelWithUnread(x, props.unreads));
+    
+    const channel = channels.find(x => x?._id === channel_id);
+    if (channel) useUnreads({ ...props, channel }, ctx);
+
+    return (
+        <div>
+            <Header placement="secondary" background style={{ background: `url('${ctx.client.servers.getBannerURL(server._id, { width: 480 }, true)}')` }}>
+                <div>
+                    { server.name }
+                </div>
+                { (permissions & ServerPermission.ManageServer) > 0 && <div className="actions">
+                    {/*<IconButton to={`/server/${server._id}/settings`}>*/}
+                        <Settings size={24} />
+                    {/*</IconButton>*/}
+                </div> }
+            </Header>
+            <ConnectionStatus />
+            <div
+                //onContextMenu={attachContextMenu('Menu', { server_list: server._id })}>
+                >
+                {channels.map(entry => {
+                    return (
+                        <Link to={`/server/${server._id}/channel/${entry._id}`}>
+                            <ChannelButton
+                                key={entry._id}
+                                channel={entry}
+                                active={channel?._id === entry._id}
+                                alert={entry.unread}
+                                compact
+                            />
+                        </Link>
+                    );
+                })}
+            </div>
+            <PaintCounter small />
+        </div>
+    )
+};
+
+export default connectState(
+    ServerSidebar,
+    state => {
+        return {
+            unreads: state.unreads
+        };
+    },
+    true
+);
diff --git a/src/components/navigation/left/common.ts b/src/components/navigation/left/common.ts
new file mode 100644
index 0000000000000000000000000000000000000000..208f795036ba2e4caf8d951fb4d87b04cf771674
--- /dev/null
+++ b/src/components/navigation/left/common.ts
@@ -0,0 +1,74 @@
+import { Channel } from "revolt.js";
+import { useLayoutEffect } from "preact/hooks";
+import { WithDispatcher } from "../../../redux/reducers";
+import { Unreads } from "../../../redux/reducers/unreads";
+import { HookContext, useForceUpdate } from "../../../context/revoltjs/hooks";
+
+type UnreadProps = WithDispatcher & {
+    channel: Channel;
+    unreads: Unreads;
+}
+
+export function useUnreads({ channel, unreads, dispatcher }: UnreadProps, context?: HookContext) {
+    const ctx = useForceUpdate(context);
+
+    useLayoutEffect(() => {
+        function checkUnread(target?: Channel) {
+            if (!target) return;
+            if (target._id !== channel._id) return;
+            if (target?.channel_type === "SavedMessages") return;
+
+            const unread = unreads[channel._id]?.last_id;
+            if (target.last_message) {
+                const message = typeof target.last_message === 'string' ? target.last_message : target.last_message._id;
+                if (!unread || (unread && message.localeCompare(unread) > 0)) {
+                    dispatcher({
+                        type: "UNREADS_MARK_READ",
+                        channel: channel._id,
+                        message,
+                        request: true
+                    });
+                }
+            }
+        }
+
+        checkUnread(channel);
+
+        ctx.client.channels.addListener("mutation", checkUnread);
+        return () => ctx.client.channels.removeListener("mutation", checkUnread);
+    }, [channel, unreads]);
+}
+
+export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
+    let last_message_id;
+    if (channel.channel_type === 'DirectMessage' || channel.channel_type === 'Group') {
+        last_message_id = channel.last_message?._id;
+    } else if (channel.channel_type === 'TextChannel') {
+        last_message_id = channel.last_message;
+    } else {
+        return { ...channel, unread: undefined, alertCount: undefined, timestamp: channel._id };
+    }
+
+    let unread: 'mention' | 'unread' | undefined;
+    let alertCount: undefined | number;
+    if (last_message_id && unreads) {
+        const u = unreads[channel._id];
+        if (u) {
+            if (u.mentions && u.mentions.length > 0) {
+                alertCount = u.mentions.length;
+                unread = 'mention';
+            } else if (u.last_id && last_message_id.localeCompare(u.last_id) > 0) {
+                unread = 'unread';
+            }
+        } else {
+            unread = 'unread';
+        }
+    }
+
+    return {
+        ...channel,
+        timestamp: last_message_id ?? channel._id,
+        unread,
+        alertCount
+    };
+}
diff --git a/src/components/ui/Category.tsx b/src/components/ui/Category.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4b6712259dfe23f94d56acd7292ccab365145b7e
--- /dev/null
+++ b/src/components/ui/Category.tsx
@@ -0,0 +1,45 @@
+import styled, { css } from "styled-components";
+import { Children } from "../../types/Preact";
+import { Plus } from "@styled-icons/feather";
+
+const CategoryBase = styled.div<Pick<Props, 'variant'>>`
+    font-size: 12px;
+    font-weight: 700;
+    text-transform: uppercase;
+
+    margin-top: 4px;
+    padding: 6px 10px;
+    margin-bottom: 4px;
+    white-space: nowrap;
+    
+    display: flex;
+    align-items: center;
+    flex-direction: row;
+    justify-content: space-between;
+
+    svg {
+        stroke: var(--foreground);
+        cursor: pointer;
+    }
+
+    ${ props => props.variant === 'uniform' && css`
+        padding-top: 6px;
+    ` }
+`;
+
+interface Props {
+    text: Children;
+    action?: () => void;
+    variant?: 'default' | 'uniform';
+}
+
+export default function Category(props: Props) {
+    return (
+        <CategoryBase>
+            {props.text}
+            {props.action && (
+                <Plus size={16} onClick={props.action} />
+            )}
+        </CategoryBase>
+    );
+};
diff --git a/src/components/ui/Header.tsx b/src/components/ui/Header.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6a97dd3aeeee46c1a2f5b48c3e156117fa57e7d6
--- /dev/null
+++ b/src/components/ui/Header.tsx
@@ -0,0 +1,32 @@
+import styled, { css } from "styled-components";
+
+interface Props {
+    background?: boolean;
+    placement: 'primary' | 'secondary'
+}
+
+export default styled.div<Props>`
+    height: 56px;
+    font-weight: 600;
+    user-select: none;
+
+    gap: 10px;
+    flex: 0 auto;
+    display: flex;
+    padding: 20px;
+    flex-shrink: 0;
+    align-items: center;
+
+    background-color: var(--primary-background);
+    background-size: cover !important;
+    background-position: center !important;
+
+    ${ props => props.background && css`
+        height: 120px;
+        align-items: flex-end;
+    ` }
+
+    ${ props => props.placement === 'secondary' && css`
+        padding: 14px;
+    ` }
+`;
diff --git a/src/context/revoltjs/hooks.ts b/src/context/revoltjs/hooks.ts
index 3f100b0388f8e4cc731e7438bd1e751f28194c33..00f8eb703ca89dcd1ba33db5437c6c9806a660d6 100644
--- a/src/context/revoltjs/hooks.ts
+++ b/src/context/revoltjs/hooks.ts
@@ -3,7 +3,7 @@ import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
 import { Client, PermissionCalculator } from 'revolt.js';
 import { AppContext } from "./RevoltClient";
 
-interface HookContext {
+export interface HookContext {
     client: Client,
     forceUpdate: () => void
 }
diff --git a/src/lib/PaintCounter.tsx b/src/lib/PaintCounter.tsx
index 81251722d3889c0e66753014886693777803c62f..e5ca30844aa251df611f93f54b8b53d96e599293 100644
--- a/src/lib/PaintCounter.tsx
+++ b/src/lib/PaintCounter.tsx
@@ -2,11 +2,17 @@ import { useState } from "preact/hooks";
 
 const counts: { [key: string]: number } = {};
 
-export default function PaintCounter() {
+export default function PaintCounter({ small }: { small?: boolean }) {
+    if (import.meta.env.PROD) return null;
+
     const [uniqueId] = useState('' + Math.random());
     const count = counts[uniqueId] ?? 0;
     counts[uniqueId] = count + 1;
     return (
-        <span>Painted {count + 1} time(s).</span>
+        <span>
+            { small ? <>P: { count + 1 }</> : <>
+                Painted {count + 1} time(s).
+            </> }
+        </span>
     )
 }
diff --git a/src/pages/App.tsx b/src/pages/App.tsx
index 6cfc6d0f0475606f6b6479792670f4de9e34914b..484a73dd4a71c4fd6d902039fc241f68439e2910 100644
--- a/src/pages/App.tsx
+++ b/src/pages/App.tsx
@@ -1,13 +1,20 @@
-import { OverlappingPanels } from "react-overlapping-panels";
+import { Docked, OverlappingPanels } from "react-overlapping-panels";
+import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
 import { Switch, Route } from "react-router-dom";
 
+import LeftSidebar from "../components/navigation/LeftSidebar";
+import RightSidebar from "../components/navigation/RightSidebar";
+
 import Home from './home/Home';
 
 export default function App() {
     return (
         <OverlappingPanels
             width="100vw"
-            height="100%">
+            height="100%"
+            leftPanel={{ width: 292, component: <LeftSidebar /> }}
+            rightPanel={{ width: 240, component: <RightSidebar /> }}
+            docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
             <Switch>
                 <Route path="/">
                     <Home />
diff --git a/src/redux/connector.tsx b/src/redux/connector.tsx
index ffe71c790b862ae89c9eb7f03619f406aec57fcf..06ee1d91a6163a22c9947f43d5f6121065075ac3 100644
--- a/src/redux/connector.tsx
+++ b/src/redux/connector.tsx
@@ -2,19 +2,22 @@
 
 import { State } from ".";
 import { h } from "preact";
-// import { memo } from "preact/compat";
+import { memo } from "preact/compat";
 import { connect, ConnectedComponent } from "react-redux";
 
 export function connectState<T>(
     component: (props: any) => h.JSX.Element | null,
     mapKeys: (state: State, props: T) => any,
-    useDispatcher?: boolean
+    useDispatcher?: boolean,
+    memoize?: boolean
 ): ConnectedComponent<(props: any) => h.JSX.Element | null, T> {
-    return (
+    let c = (
         useDispatcher
             ? connect(mapKeys, (dispatcher) => {
                   return { dispatcher };
               })
             : connect(mapKeys)
-    )(component); //(memo(component));
+    )(component);
+    
+    return memoize ? memo(c) : c;
 }
diff --git a/src/styles/index.scss b/src/styles/index.scss
index d07896c1c562b7cac9570ba8b3e77ee6e703a3c5..7869c3d3aeb57b63e648a860c975148a722a80b7 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -1,3 +1,5 @@
 @import "elements";
 @import "fonts";
 @import "page";
+
+@import "react-overlapping-panels/dist"
diff --git a/yarn.lock b/yarn.lock
index 76c2a99285a8816571f9463a2c871f71af58859a..3906148e109ebd7cd2ff5d12ec8a87745ecec379 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1597,6 +1597,11 @@ chalk@^4.0.0:
   optionalDependencies:
     fsevents "~2.3.2"
 
+classnames@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
+  integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
+
 color-convert@^1.9.0:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"