diff --git a/src/components/common/AutoComplete.tsx b/src/components/common/AutoComplete.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5908dcb65d0c3581876f769de16a1bc66f56cccb
--- /dev/null
+++ b/src/components/common/AutoComplete.tsx
@@ -0,0 +1,364 @@
+import { StateUpdater, useContext, useState } from "preact/hooks";
+import { AppContext } from "../../context/revoltjs/RevoltClient";
+import { emojiDictionary } from "../../assets/emojis";
+import UserIcon from "./user/UserIcon";
+import styled from "styled-components";
+import { SYSTEM_USER_ID, User } from "revolt.js";
+import Emoji from "./Emoji";
+
+export type AutoCompleteState =
+    | { type: "none" }
+    | {
+        type: "emoji";
+        matches: string[];
+        selected: number;
+        within: boolean;
+    }
+    | {
+        type: "user";
+        matches: User[];
+        selected: number;
+        within: boolean;
+    };
+
+export type SearchClues = {
+    users?: { type: 'channel', id: string } | { type: 'all' }
+};
+
+export type AutoCompleteProps = {
+    state: AutoCompleteState,
+    setState: StateUpdater<AutoCompleteState>,
+
+    onKeyUp: (ev: KeyboardEvent) => void,
+    onKeyDown: (ev: KeyboardEvent) => boolean,
+    onChange: (ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) => void,
+    onClick: JSX.MouseEventHandler<HTMLButtonElement>,
+    onFocus: JSX.FocusEventHandler<HTMLTextAreaElement>,
+    onBlur: JSX.FocusEventHandler<HTMLTextAreaElement>
+}
+
+export function useAutoComplete(setValue: (v?: string) => void, searchClues?: SearchClues): AutoCompleteProps {
+    const [state, setState] = useState<AutoCompleteState>({ type: 'none' });
+    const [focused, setFocused] = useState(false);
+    const client = useContext(AppContext);
+
+    function findSearchString(
+        el: HTMLTextAreaElement
+    ): ["emoji" | "user", string, number] | undefined {
+        if (el.selectionStart === el.selectionEnd) {
+            let cursor = el.selectionStart;
+            let content = el.value.slice(0, cursor);
+
+            let valid = /\w/;
+
+            let j = content.length - 1;
+            if (content[j] === '@') {
+                return [
+                    "user",
+                    "",
+                    j
+                ];
+            }
+
+            while (j >= 0 && valid.test(content[j])) {
+                j--;
+            }
+
+            if (j === -1) return;
+            let current = content[j];
+
+            if (current === ":" || current === "@") {
+                let search = content.slice(j + 1, content.length);
+                if (search.length > 0) {
+                    return [
+                        current === ":" ? "emoji" : "user",
+                        search.toLowerCase(),
+                        j + 1
+                    ];
+                }
+            }
+        }
+    }
+    
+    function onChange(ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) {
+        const el = ev.currentTarget;
+
+        let result = findSearchString(el);
+        if (result) {
+            let [type, search] = result;
+            const regex = new RegExp(search, 'i');
+
+            if (type === "emoji") {
+                // ! FIXME: we should convert it to a Binary Search Tree and use that
+                let matches = Object.keys(emojiDictionary)
+                    .filter((emoji: string) => emoji.match(regex))
+                    .splice(0, 5);
+
+                if (matches.length > 0) {
+                    let currentPosition =
+                        state.type !== "none"
+                            ? state.selected
+                            : 0;
+                    
+                    setState({
+                        type: "emoji",
+                        matches,
+                        selected: Math.min(currentPosition, matches.length - 1),
+                        within: false
+                    });
+
+                    return;
+                }
+            }
+
+            if (type === "user" && searchClues?.users) {
+                let users: User[] = [];
+                switch (searchClues.users.type) {
+                    case 'all': users = client.users.toArray(); break;
+                    case 'channel': {
+                        let channel = client.channels.get(searchClues.users.id);
+                        switch (channel?.channel_type) {
+                            case 'Group':
+                            case 'DirectMessage':
+                                users = client.users.mapKeys(channel.recipients)
+                                    .filter(x => typeof x !== 'undefined') as User[];
+                                break;
+                            case 'TextChannel':
+                                const server = channel.server;
+                                users = client.servers.members.toArray()
+                                    .filter(x => x._id.substr(0, 26) === server)
+                                    .map(x => client.users.get(x._id.substr(26)))
+                                    .filter(x => typeof x !== 'undefined') as User[];
+                                break;
+                            default: return;
+                        }
+                    }
+                }
+
+                users = users.filter(x => x._id !== SYSTEM_USER_ID);
+
+                let matches = (search.length > 0 ? users.filter(user => user?.username.toLowerCase().match(regex)) : users)
+                    .splice(0, 5)
+                    .filter(x => typeof x !== "undefined");
+
+                if (matches.length > 0) {
+                    let currentPosition =
+                        state.type !== "none"
+                            ? state.selected
+                            : 0;
+                    
+                    setState({
+                        type: "user",
+                        matches,
+                        selected: Math.min(currentPosition, matches.length - 1),
+                        within: false
+                    });
+
+                    return;
+                }
+            }
+        }
+
+        if (state.type !== "none") {
+            setState({ type: "none" });
+        }
+    }
+
+    function selectCurrent(el: HTMLTextAreaElement) {
+        if (state.type !== "none") {
+            let result = findSearchString(el);
+            if (result) {
+                let [_type, search, index] = result;
+
+                let content = el.value.split("");
+                if (state.type === "emoji") {
+                    content.splice(
+                        index,
+                        search.length,
+                        state.matches[state.selected],
+                        ": "
+                    );
+                } else {
+                    content.splice(
+                        index - 1,
+                        search.length + 1,
+                        "<@",
+                        state.matches[state.selected]._id,
+                        "> "
+                    );
+                }
+
+                setValue(content.join(""));
+            }
+        }
+    }
+
+    function onClick(ev: JSX.TargetedMouseEvent<HTMLButtonElement>) {
+        ev.preventDefault();
+        selectCurrent(document.querySelector("#message")!);
+    }
+
+    function onKeyDown(e: KeyboardEvent) {
+        if (focused && state.type !== 'none') {
+            if (e.key === "ArrowUp") {
+                e.preventDefault();
+                if (state.selected > 0) {
+                    setState({
+                        ...state,
+                        selected: state.selected - 1
+                    });
+                }
+
+                return true;
+            }
+
+            if (e.key === "ArrowDown") {
+                e.preventDefault();
+                if (state.selected < state.matches.length - 1) {
+                    setState({
+                        ...state,
+                        selected: state.selected + 1
+                    });
+                }
+
+                return true;
+            }
+
+            if (e.key === "Enter" || e.key === "Tab") {
+                e.preventDefault();
+                selectCurrent(
+                    e.currentTarget as HTMLTextAreaElement
+                );
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    function onKeyUp(e: KeyboardEvent) {
+        if (e.currentTarget !== null) {
+            // @ts-expect-error
+            onChange(e);
+        }
+    }
+
+    function onFocus(ev: JSX.TargetedFocusEvent<HTMLTextAreaElement>) {
+        setFocused(true);
+        onChange(ev);
+    }
+
+    function onBlur() {
+        if (state.type !== 'none' && state.within) return;
+        setFocused(false);
+    }
+
+    return {
+        state: focused ? state : { type: 'none' },
+        setState,
+
+        onClick,
+        onChange,
+        onKeyUp,
+        onKeyDown,
+        onFocus,
+        onBlur
+    }
+}
+
+const Base = styled.div`
+    position: relative;
+
+    > div {
+        bottom: 0;
+        width: 100%;
+        position: absolute;
+        background: var(--primary-header);
+    }
+
+    button {
+        gap: 8px;
+        margin: 4px;
+        padding: 6px;
+        border: none;
+        display: flex;
+        cursor: pointer;
+        border-radius: 6px;
+        flex-direction: row;
+        background: transparent;
+        color: var(--foreground);
+        width: calc(100% - 12px);
+
+        span {
+            display: grid;
+            place-items: center;
+        }
+
+        &.active {
+            background: var(--primary-background);
+        }
+    }
+`;
+
+export default function AutoComplete({ state, setState, onClick }: Pick<AutoCompleteProps, 'state' | 'setState' | 'onClick'>) {
+    return (
+        <Base>
+            <div>
+                {state.type === "emoji" &&
+                    state.matches.map((match, i) => (
+                        <button
+                            className={i === state.selected ? "active" : ''}
+                            onMouseEnter={() =>
+                                (i !== state.selected ||
+                                    !state.within) &&
+                                    setState({
+                                    ...state,
+                                    selected: i,
+                                    within: true
+                                })
+                            }
+                            onMouseLeave={() =>
+                                state.within &&
+                                setState({
+                                    ...state,
+                                    within: false
+                                })
+                            }
+                            onClick={onClick}>
+                            <Emoji emoji={(emojiDictionary as any)[match]} size={20} />
+                            :{match}:
+                        </button>
+                    ))}
+                {state.type === "user" &&
+                    state.matches.map((match, i) => (
+                        <button
+                            className={i === state.selected ? "active" : ''}
+                            onMouseEnter={() =>
+                                (i !== state.selected ||
+                                    !state.within) &&
+                                setState({
+                                    ...state,
+                                    selected: i,
+                                    within: true
+                                })
+                            }
+                            onMouseLeave={() =>
+                                state.within &&
+                                setState({
+                                    ...state,
+                                    within: false
+                                })
+                            }
+                            onClick={onClick}>
+                            <UserIcon
+                                size={24}
+                                target={match}
+                                status={true} />
+                            {match.username}
+                        </button>
+                    ))}
+            </div>
+        </Base>
+    )
+}
diff --git a/src/components/markdown/Emoji.tsx b/src/components/common/Emoji.tsx
similarity index 92%
rename from src/components/markdown/Emoji.tsx
rename to src/components/common/Emoji.tsx
index b7043e7f07bfbde27f9892405b0d54dc3484b636..74e381b5aa161fa3f13892030ff7a4aaa068e4bc 100644
--- a/src/components/markdown/Emoji.tsx
+++ b/src/components/common/Emoji.tsx
@@ -24,7 +24,7 @@ function parseEmoji(emoji: string) {
     return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`;
 }
 
-export function Emoji({ emoji, size }: { emoji: string, size?: number }) {
+export default function Emoji({ emoji, size }: { emoji: string, size?: number }) {
     return (
         <img
             alt={emoji}
diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx
index 87dfeedd82b9ad7de0cb6b8d8b167de524c10551..362c08758ccdc25b4cefb712faf5c83356620de0 100644
--- a/src/components/common/messaging/Message.tsx
+++ b/src/components/common/messaging/Message.tsx
@@ -10,6 +10,8 @@ import { QueuedMessage } from "../../../redux/reducers/queue";
 import { MessageObject } from "../../../context/revoltjs/util";
 import MessageBase, { MessageContent, MessageDetail, MessageInfo } from "./MessageBase";
 import Overline from "../../ui/Overline";
+import { useContext } from "preact/hooks";
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
 
 interface Props {
     attachContext?: boolean
@@ -23,7 +25,8 @@ interface Props {
 export default function Message({ attachContext, message, contrast, content: replacement, head, queued }: Props) {
     // TODO: Can improve re-renders here by providing a list
     // TODO: of dependencies. We only need to update on u/avatar.
-    let user = useUser(message.author);
+    const user = useUser(message.author);
+    const client = useContext(AppContext);
 
     const content = message.content as string;
     return (
@@ -31,6 +34,7 @@ export default function Message({ attachContext, message, contrast, content: rep
             head={head}
             contrast={contrast}
             sending={typeof queued !== 'undefined'}
+            mention={message.mentions?.includes(client.user!._id)}
             failed={typeof queued?.error !== 'undefined'}
             onContextMenu={attachContext ? attachContextMenu('Menu', { message, contextualChannel: message.channel, queued }) : undefined}>
             <MessageInfo>
diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx
index e938e73cad90c157a5496347081b8553f9fe1b70..4260bdea8ec3516481eac28d6408bbcf3bf93c74 100644
--- a/src/components/common/messaging/MessageBox.tsx
+++ b/src/components/common/messaging/MessageBox.tsx
@@ -20,6 +20,7 @@ import { SingletonMessageRenderer, SMOOTH_SCROLL_ON_RECEIVE } from "../../../lib
 
 import FilePreview from './bars/FilePreview';
 import { debounce } from "../../../lib/debounce";
+import AutoComplete, { useAutoComplete } from "../AutoComplete";
 
 type Props = WithDispatcher & {
     channel: Channel;
@@ -226,9 +227,11 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
     }
 
     const debouncedStopTyping = useCallback(debounce(stopTyping, 1000), [ channel._id ]);
+    const { onChange, onKeyUp, onKeyDown, onFocus, onBlur, ...autoCompleteProps } = useAutoComplete(setMessage, { users: { type: 'channel', id: channel._id } });
 
     return (
         <>
+            <AutoComplete {...autoCompleteProps} />
             <FilePreview state={uploadState} addFile={() => uploadState.type === 'attached' &&
                 grabFiles(20_000_000, files => setUploadState({ type: 'attached', files: [ ...uploadState.files, ...files ] }),
                     () => openScreen({ id: "error", error: "FileTooLarge" }), true)}
@@ -271,7 +274,10 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
                     padding={14}
                     id="message"
                     value={draft ?? ''}
+                    onKeyUp={onKeyUp}
                     onKeyDown={e => {
+                        if (onKeyDown(e)) return;
+
                         if (
                             e.key === "ArrowUp" &&
                             (!draft || draft.length === 0)
@@ -298,7 +304,10 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
                     onChange={e => {
                         setMessage(e.currentTarget.value);
                         startTyping();
-                    }} />
+                        onChange(e);
+                    }}
+                    onFocus={onFocus}
+                    onBlur={onBlur} />
                 <Action>
                     <IconButton onClick={send}>
                         <Send size={20} />
diff --git a/src/components/markdown/Renderer.tsx b/src/components/markdown/Renderer.tsx
index c2a859290d43a49b1dd540d377d7a057e73107d2..287bb4a5d7d0235658601d5140f4a9f004fdad97 100644
--- a/src/components/markdown/Renderer.tsx
+++ b/src/components/markdown/Renderer.tsx
@@ -1,10 +1,11 @@
 import MarkdownIt from "markdown-it";
 import { RE_MENTIONS } from "revolt.js";
-import { generateEmoji } from "./Emoji";
 import { useContext } from "preact/hooks";
 import { MarkdownProps } from "./Markdown";
 import styles from "./Markdown.module.scss";
+import { generateEmoji } from "../common/Emoji";
 import { internalEmit } from "../../lib/eventEmitter";
+import { emojiDictionary } from "../../assets/emojis";
 import { AppContext } from "../../context/revoltjs/RevoltClient";
 
 import Prism from "prismjs";
@@ -47,7 +48,7 @@ export const md: MarkdownIt = MarkdownIt({
     }
 })
 .disable("image")
-.use(MarkdownEmoji/*, { defs: emojiDictionary }*/)
+.use(MarkdownEmoji, { defs: emojiDictionary })
 .use(MarkdownSpoilers)
 .use(MarkdownSup)
 .use(MarkdownSub)
diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx
index da9c1571ebe9a1c923329fa514b09a795244f947..26c90bc79847e477632f0cc35eccf76ca96d5707 100644
--- a/src/context/revoltjs/RevoltClient.tsx
+++ b/src/context/revoltjs/RevoltClient.tsx
@@ -113,6 +113,11 @@ function Context({ auth, sync, children, dispatcher }: Props) {
                 dispatcher({ type: "LOGOUT" });
 
                 delete client.user;
+                // ! FIXME: write procedure client.clear();
+                client.users.clear();
+                client.channels.clear();
+                client.servers.clear();
+                client.servers.members.clear();
                 dispatcher({ type: "RESET" });
 
                 openScreen({ id: "none" });
diff --git a/src/pages/settings/panes/Languages.tsx b/src/pages/settings/panes/Languages.tsx
index 3c58b23ffecfd2bf6c9012cbcb946f8ab77de87d..7e5566b33bfc0e96ab1508589e6c0ad4d1971979 100644
--- a/src/pages/settings/panes/Languages.tsx
+++ b/src/pages/settings/panes/Languages.tsx
@@ -1,10 +1,10 @@
 import { Text } from "preact-i18n";
 import styles from "./Panes.module.scss";
 import Tip from "../../../components/ui/Tip";
+import Emoji from "../../../components/common/Emoji";
 import Checkbox from "../../../components/ui/Checkbox";
 import { connectState } from "../../../redux/connector";
 import { WithDispatcher } from "../../../redux/reducers";
-import { Emoji } from "../../../components/markdown/Emoji";
 import { Language, LanguageEntry, Languages as Langs } from "../../../context/Locale";
 
 interface Props {