diff --git a/src/plugins/notesSearcher/README.md b/src/plugins/notesSearcher/README.md
new file mode 100644
index 000000000..332658fbd
--- /dev/null
+++ b/src/plugins/notesSearcher/README.md
@@ -0,0 +1,7 @@
+# NotesSearcher
+
+Allows you to open a modal with all of your notes and search through them by user ID, note text, and username
+
+## Preview
+
+![preview](https://i.imgur.com/yBwhcx6.png)
diff --git a/src/plugins/notesSearcher/components/Icons.tsx b/src/plugins/notesSearcher/components/Icons.tsx
new file mode 100644
index 000000000..90543feb1
--- /dev/null
+++ b/src/plugins/notesSearcher/components/Icons.tsx
@@ -0,0 +1,76 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { LazyComponent } from "@utils/lazyReact";
+import { React } from "@webpack/common";
+
+export const NotesDataIcon = LazyComponent(() => React.memo(() => {
+ return (
+
+ );
+}));
+
+export const SaveIcon = LazyComponent(() => React.memo(() => {
+ return (
+
+ );
+}));
+
+export const DeleteIcon = LazyComponent(() => React.memo(() => {
+ return (
+
+ );
+}));
+
+export const RefreshIcon = LazyComponent(() => React.memo(() => {
+ return (
+
+ );
+}));
+
+export const PopupIcon = LazyComponent(() => React.memo(() => {
+ return (
+
+ );
+}));
+
+export const CrossIcon = LazyComponent(() => React.memo(() => {
+ return (
+
+ );
+}));
+
+export const ProblemIcon = LazyComponent(() => React.memo(() => {
+ return (
+
+ );
+}));
+
+export const SuccessIcon = LazyComponent(() => React.memo(() => {
+ return (
+
+ );
+}));
diff --git a/src/plugins/notesSearcher/components/LoadingSpinner.tsx b/src/plugins/notesSearcher/components/LoadingSpinner.tsx
new file mode 100644
index 000000000..dd773cb5b
--- /dev/null
+++ b/src/plugins/notesSearcher/components/LoadingSpinner.tsx
@@ -0,0 +1,29 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { LazyComponent } from "@utils/react";
+import { React } from "@webpack/common";
+
+export const LoadingSpinner = LazyComponent(() => React.memo(() => {
+ return (
+
+
+
+ );
+}));
diff --git a/src/plugins/notesSearcher/components/NotesDataButton.tsx b/src/plugins/notesSearcher/components/NotesDataButton.tsx
new file mode 100644
index 000000000..758dd0d8a
--- /dev/null
+++ b/src/plugins/notesSearcher/components/NotesDataButton.tsx
@@ -0,0 +1,25 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { LazyComponent } from "@utils/react";
+import { findExportedComponentLazy } from "@webpack";
+import { React } from "@webpack/common";
+
+import { NotesDataIcon } from "./Icons";
+import { openNotesDataModal } from "./NotesDataModal";
+
+const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider");
+
+export const OpenNotesDataButton = LazyComponent(() => React.memo(() => {
+ return (
+ openNotesDataModal()}
+ tooltip={"View Notes"}
+ icon={NotesDataIcon}
+ />
+ );
+}));
diff --git a/src/plugins/notesSearcher/components/NotesDataModal.tsx b/src/plugins/notesSearcher/components/NotesDataModal.tsx
new file mode 100644
index 000000000..f82a58bce
--- /dev/null
+++ b/src/plugins/notesSearcher/components/NotesDataModal.tsx
@@ -0,0 +1,217 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { classNameFactory } from "@api/Styles";
+import {
+ closeModal, ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, openModal
+} from "@utils/modal";
+import { LazyComponent } from "@utils/react";
+import { Button, React, RelationshipStore, Select, Text, TextInput, useCallback, useMemo, useReducer, useState } from "@webpack/common";
+
+import { cacheUsers, getNotes, usersCache as usersCache$1 } from "../data";
+import NotesDataRow from "./NotesDataRow";
+
+const cl = classNameFactory("vc-notes-searcher-modal-");
+
+const enum SearchStatus {
+ ALL,
+ FRIENDS,
+ BLOCKED,
+}
+
+const filterUser = (query: string, userId: string, userNotes: string) => {
+ if (query === "" || userId.includes(query)) return true;
+
+ query = query.toLowerCase();
+
+ const user = usersCache$1.get(userId);
+
+ return user && (
+ user.globalName?.toLowerCase().includes(query) || user.username.toLowerCase().includes(query)
+ ) || userNotes.toLowerCase().includes(query);
+};
+
+// looks like a shit but I don't know better way to do it
+// P.S. using `getNotes()` as deps for useMemo won't work due to object init outside of component
+let RefreshNotesDataEx: () => void | undefined;
+
+export const refreshNotesData = () => {
+ if (!RefreshNotesDataEx) return;
+
+ RefreshNotesDataEx();
+};
+
+export function NotesDataModal({ modalProps, close }: {
+ modalProps: ModalProps;
+ close(): void;
+}) {
+ const [searchValue, setSearchValue] = useState({ query: "", status: SearchStatus.ALL });
+
+ const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, query }));
+ const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
+
+ const [usersNotesData, refreshNotesData] = useReducer(() => {
+ return Object.entries(getNotes())
+ .map<[string, string]>(([userId, { note }]) => [userId, note])
+ .filter((([_, note]) => note && note !== ""));
+ },
+ Object.entries(getNotes())
+ .map<[string, string]>(([userId, { note }]) => [userId, note])
+ .filter((([_, note]) => note && note !== ""))
+ );
+
+ RefreshNotesDataEx = refreshNotesData;
+
+ const filteredNotes = useMemo(() => {
+ const { query, status } = searchValue;
+
+ if (query === "" && status === SearchStatus.ALL) {
+ return usersNotesData;
+ }
+
+ return usersNotesData
+ .filter(([userId, userNotes]) => {
+ switch (status) {
+ case SearchStatus.FRIENDS:
+ return RelationshipStore.isFriend(userId) && filterUser(query, userId, userNotes);
+ case SearchStatus.BLOCKED:
+ return RelationshipStore.isBlocked(userId) && filterUser(query, userId, userNotes);
+ default:
+ return filterUser(query, userId, userNotes);
+ }
+ });
+ }, [usersNotesData, searchValue]);
+
+ const [visibleNotesNum, setVisibleNotesNum] = useState(10);
+
+ const loadMore = useCallback(() => {
+ setVisibleNotesNum(prevNum => prevNum + 10);
+ }, []);
+
+ const visibleNotes = filteredNotes.slice(0, visibleNotesNum);
+
+ const canLoadMore = visibleNotesNum < filteredNotes.length;
+
+ return (
+
+
+ User Notes
+
+
+
+
+
+
+ {
+ modalProps.transitionState === 1 &&
+
+ {
+ !visibleNotes.length ? : (
+
+ )
+ }
+
+ }
+
+
+ );
+}
+
+// looks like a shit but I don't know better way to do it
+// P.S. using `usersCache` as deps for useMemo won't work due to object init outside of component
+let RefreshUsersCacheEx: () => void | undefined;
+
+export const refreshUsersCache = () => {
+ if (!RefreshUsersCacheEx) return;
+
+ RefreshUsersCacheEx();
+};
+
+const NotesDataContent = ({ visibleNotes, canLoadMore, loadMore, refreshNotesData }: {
+ visibleNotes: [string, string][];
+ canLoadMore: boolean;
+ loadMore(): void;
+ refreshNotesData(): void;
+}) => {
+ if (!visibleNotes.length)
+ return ;
+
+ const [usersCache, refreshUsersCache] = useReducer(() => {
+ return new Map(usersCache$1);
+ }, usersCache$1);
+
+ RefreshUsersCacheEx = refreshUsersCache;
+
+ return (
+
+ {
+ visibleNotes
+ .map(([userId, userNotes]) => {
+ return (
+
+ );
+ })
+ }
+ {
+ canLoadMore &&
+
+ }
+
+ );
+};
+
+const NoNotes = LazyComponent(() => React.memo(() => (
+
+
+ No Notes.
+
+
+)));
+
+let fistTimeOpen = true;
+
+export const openNotesDataModal = async () => {
+ if (fistTimeOpen) {
+ cacheUsers();
+ fistTimeOpen = false;
+ }
+
+ const key = openModal(modalProps => (
+ closeModal(key)}
+ />
+ ));
+};
diff --git a/src/plugins/notesSearcher/components/NotesDataRow.tsx b/src/plugins/notesSearcher/components/NotesDataRow.tsx
new file mode 100644
index 000000000..9271959d0
--- /dev/null
+++ b/src/plugins/notesSearcher/components/NotesDataRow.tsx
@@ -0,0 +1,248 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { classNameFactory } from "@api/Styles";
+import { openPrivateChannel, openUserProfile } from "@utils/discord";
+import { copyWithToast } from "@utils/misc";
+import { Alerts, Avatar, Button, ContextMenuApi, Menu, React, Text, TextArea, Tooltip, useState } from "@webpack/common";
+
+import { updateNote } from "../data";
+import { UsersCache } from "../types";
+import { DeleteIcon, PopupIcon, RefreshIcon, SaveIcon } from "./Icons";
+import { LoadingSpinner } from "./LoadingSpinner";
+
+const cl = classNameFactory("vc-notes-searcher-modal-");
+
+export default ({ userId, userNotes: userNotesArg, refreshNotesData, usersCache }: {
+ userId: string;
+ userNotes: string;
+ refreshNotesData(): void;
+ usersCache: UsersCache;
+}) => {
+ let userCache = usersCache.get(userId);
+
+ const pending = !userCache;
+
+ userCache ??= {
+ id: userId,
+ globalName: "Loading...",
+ username: "Loading...",
+ avatar: "https://cdn.discordapp.com/embed/avatars/4.png",
+ };
+
+ const [userNotes, setUserNotes] = useState(userNotesArg);
+
+ return (
+ {
+ ContextMenuApi.openContextMenu(event, () =>
+
+ openUserProfile(userId)}
+ />
+ openPrivateChannel(userId)}
+ />
+ copyWithToast(userCache!.id)}
+ />
+ {
+ !pending &&
+ (
+ <>
+ copyWithToast(userCache!.globalName ?? userCache!.username)}
+ />
+ copyWithToast(userCache!.username)}
+ />
+ copyWithToast(userCache!.avatar)}
+ />
+ >
+ )
+ }
+ copyWithToast(userNotes)}
+ />
+
+ );
+ }}
+ >
+ {
+ pending ?
:
+
+ }
+
+ {userCache.globalName}
+ {userCache.username}
+ {userCache.id}
+
+
+
+
+
+ {({ onMouseLeave, onMouseEnter }) => (
+
+ )}
+
+
+ {({ onMouseLeave, onMouseEnter }) => (
+
+ )}
+
+
+ {({ onMouseLeave, onMouseEnter }) => (
+
+ )}
+
+
+ {({ onMouseLeave, onMouseEnter }) => (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/plugins/notesSearcher/data.ts b/src/plugins/notesSearcher/data.ts
new file mode 100644
index 000000000..5180989f7
--- /dev/null
+++ b/src/plugins/notesSearcher/data.ts
@@ -0,0 +1,153 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { Constants, FluxDispatcher, GuildStore, RestAPI, SnowflakeUtils, UserStore, UserUtils } from "@webpack/common";
+import { waitForStore } from "webpack/common/internal";
+
+import { refreshNotesData, refreshUsersCache } from "./components/NotesDataModal";
+import * as t from "./types";
+
+let NoteStore: t.NoteStore;
+
+waitForStore("NoteStore", s => NoteStore = s);
+
+export const getNotes = () => {
+ return NoteStore.getNotes();
+};
+
+export const onNoteUpdate = () => {
+ refreshNotesData();
+};
+
+export const updateNote = (userId: string, note: string | null) => {
+ RestAPI.put({
+ url: Constants.Endpoints.NOTE(userId),
+ body: { note: note },
+ oldFormErrors: true
+ });
+};
+
+export const usersCache: t.UsersCache = new Map();
+
+export const onUserUpdate = ({ user }: { user: t.User; }) => {
+ if (!getNotes()[user.id]) return;
+
+ // doesn't have .getAvatarURL
+ const userFromStore = UserStore.getUser(user.id);
+
+ if (!userFromStore) return;
+
+ cacheUser(userFromStore);
+};
+
+const fetchUser = async (userId: string) => {
+ for (let _ = 0; _ < 5; _++) {
+ try {
+ return await UserUtils.getUser(userId);
+ } catch (error: any) {
+ const wait = error?.body?.retry_after;
+
+ if (!wait) return;
+
+ await new Promise(resolve => setTimeout(resolve, wait * 1000 + 100));
+ }
+ }
+};
+
+const cacheUser = (user: t.User) => {
+ usersCache.set(user.id, {
+ id: user.id,
+ globalName: user.globalName ?? user.username,
+ username: user.username,
+ avatar: user.getAvatarURL(void 0, void 0, false),
+ });
+};
+
+export const cacheUsers = async () => {
+ const toRequest: string[] = [];
+
+ for (const userId of Object.keys(getNotes())) {
+ const user = UserStore.getUser(userId);
+
+ if (user) {
+ cacheUser(user);
+ continue;
+ }
+
+ toRequest.push(userId);
+ }
+
+ if (usersCache.size >= Object.keys(getNotes()).length) {
+ return;
+ }
+
+ const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());
+
+ const allGuildIds = Object.keys(GuildStore.getGuilds());
+ let count = allGuildIds.length * Math.ceil(toRequest.length / 100);
+
+ const processed = new Set();
+
+ const callback = async ({ chunks }) => {
+ for (const chunk of chunks) {
+ const { nonce, members }: {
+ nonce: string;
+ members: {
+ user: t.User;
+ }[];
+ } = chunk;
+
+ if (nonce !== sentNonce) {
+ return;
+ }
+
+ members.forEach(({ user }) => {
+ if (processed.has(user.id)) return;
+
+ processed.add(user.id);
+
+ cacheUser(UserStore.getUser(user.id));
+ });
+
+ refreshUsersCache();
+
+ if (--count === 0) {
+ FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
+
+ const userIds = Object.keys(getNotes());
+
+ if (usersCache.size !== userIds.length) {
+
+ for (const userId of userIds) {
+ if (usersCache.has(userId)) continue;
+
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ const user = await fetchUser(userId);
+
+ if (user) {
+ cacheUser(user);
+ refreshUsersCache();
+ }
+ }
+
+ } else
+ refreshUsersCache();
+ }
+ }
+ };
+
+ FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
+
+ for (let i = 0; i < toRequest.length; i += 100) {
+ FluxDispatcher.dispatch({
+ type: "GUILD_MEMBERS_REQUEST",
+ guildIds: allGuildIds,
+ userIds: toRequest.slice(i, i + 100),
+ nonce: sentNonce,
+ });
+ }
+};
diff --git a/src/plugins/notesSearcher/index.tsx b/src/plugins/notesSearcher/index.tsx
new file mode 100644
index 000000000..d894227b1
--- /dev/null
+++ b/src/plugins/notesSearcher/index.tsx
@@ -0,0 +1,128 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { disableStyle, enableStyle } from "@api/Styles";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+import { FluxDispatcher } from "@webpack/common";
+
+import { OpenNotesDataButton } from "./components/NotesDataButton";
+import { getNotes, onNoteUpdate, onUserUpdate } from "./data";
+import settings from "./settings";
+import styles from "./styles.css?managed";
+import { Notes } from "./types";
+
+export default definePlugin({
+ name: "NotesSearcher",
+ description: "Allows you to open a modal with all of your notes and search through them by user ID, note text, and username",
+ authors: [Devs.Vishnya],
+ settings,
+ patches: [
+ {
+ find: "toolbar:function",
+ replacement: {
+ match: /(function \i\(\i\){)(.{1,200}toolbar.{1,100}mobileToolbar)/,
+ replace: "$1$self.addToolBarButton(arguments[0]);$2",
+ },
+ },
+ {
+ find: '="NoteStore",',
+ replacement: [
+ {
+ match: /getNote\(\i\){return (\i)/,
+ replace: "getNotes(){return $1}$&",
+ },
+ // not sure it won't break anything but should be fine
+ {
+ match: /CONNECTION_OPEN:\i,OVERLAY_INITIALIZE:\i,/,
+ replace: "",
+ },
+ ],
+ },
+ {
+ find: ".REQUEST_GUILD_MEMBERS",
+ replacement: {
+ match: /\.send\(8,{(?!nonce)/,
+ replace: "$&nonce:arguments[1].nonce,",
+ },
+ },
+ {
+ find: "GUILD_MEMBERS_REQUEST:",
+ replacement: {
+ match: /presences:!!(\i)\.presences(?!,nonce)/,
+ replace: "$&,nonce:$1.nonce",
+ },
+ },
+ {
+ find: ".not_found",
+ replacement: {
+ match: /notFound:(\i)\.not_found(?!,nonce)/,
+ replace: "$&,nonce:$1.nonce",
+ },
+ },
+ {
+ find: "[IDENTIFY]",
+ replacement: {
+ match: /capabilities:(\i\.\i),/,
+ replace: "capabilities:$1&~1,",
+ },
+ },
+ {
+ find: "_handleDispatch",
+ replacement: {
+ match: /let \i=(\i).session_id;/,
+ replace: "$&$self.ready($1);",
+ },
+ },
+ ],
+
+ start: async () => {
+ FluxDispatcher.subscribe("USER_NOTE_UPDATE", onNoteUpdate);
+ FluxDispatcher.subscribe("USER_UPDATE", onUserUpdate);
+
+ enableStyle(styles);
+ },
+ stop: () => {
+ FluxDispatcher.unsubscribe("USER_NOTE_UPDATE", onNoteUpdate);
+ FluxDispatcher.unsubscribe("USER_UPDATE", onUserUpdate);
+
+ disableStyle(styles);
+ },
+
+ ready: ({ notes }: { notes: { [userId: string]: string; }; }) => {
+ const notesFromStore = getNotes();
+
+ for (const userId of Object.keys(notesFromStore)) {
+ delete notesFromStore[userId];
+ }
+
+ Object.assign(notesFromStore, Object.entries(notes).reduce((newNotes, [userId, note]) => {
+ newNotes[userId] = {
+ note,
+ loading: false,
+ };
+
+ return newNotes;
+ }, {} as Notes));
+ },
+
+ addToolBarButton: (children: { toolbar: React.ReactNode[] | React.ReactNode; }) => {
+ if (Array.isArray(children.toolbar))
+ return children.toolbar.push(
+
+
+
+ );
+
+ children.toolbar = [
+
+
+ ,
+ children.toolbar,
+ ];
+ },
+});
diff --git a/src/plugins/notesSearcher/settings.tsx b/src/plugins/notesSearcher/settings.tsx
new file mode 100644
index 000000000..986d1bc93
--- /dev/null
+++ b/src/plugins/notesSearcher/settings.tsx
@@ -0,0 +1,16 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { definePluginSettings } from "@api/Settings";
+import { OptionType } from "@utils/types";
+
+export default definePluginSettings({
+ startupCache: {
+ default: false,
+ type: OptionType.BOOLEAN,
+ description: "Cache all users on startup",
+ },
+});
diff --git a/src/plugins/notesSearcher/styles.css b/src/plugins/notesSearcher/styles.css
new file mode 100644
index 000000000..155caf219
--- /dev/null
+++ b/src/plugins/notesSearcher/styles.css
@@ -0,0 +1,107 @@
+.vc-notes-searcher-toolbox-button svg {
+ color: var(--interactive-normal);
+}
+
+.vc-notes-searcher-modal-user-actions * svg {
+ width: 32px !important;
+ height: 32px !important;
+}
+
+.vc-notes-searcher-toolbox-button:hover svg,
+.vc-notes-searcher-toolbox-button[class*="selected"] svg {
+ color: var(--interactive-active);
+}
+
+.vc-notes-searcher-modal-root {
+ min-height: 75vh;
+ max-height: 75vh;
+ min-width: 70vw;
+ max-width: 70vw;
+}
+
+.vc-notes-searcher-modal-header-input {
+ width: 100%;
+ margin-right: 16px;
+}
+
+.vc-notes-searcher-modal-content {
+ padding-bottom: 16px;
+ height: 100%;
+}
+
+.vc-notes-searcher-modal-content div[aria-hidden="true"] {
+ display: none;
+}
+
+.vc-notes-searcher-modal-content-inner > *:not(:last-child) {
+ margin-bottom: 8px;
+}
+
+.vc-notes-searcher-modal-user:hover:not(
+:has(
+ .vc-notes-searcher-modal-user-text-area:hover,
+ .vc-notes-searcher-modal-user-actions:hover
+)
+) {
+ background-color: var(--background-secondary-alt);
+}
+
+.vc-notes-searcher-modal-user-avatar {
+ aspect-ratio: 1 / 1;
+ margin: 12px;
+}
+
+.vc-notes-searcher-modal-user-actions * div:has(svg) {
+ width: 32px !important;
+ height: 32px !important;
+ overflow: visible !important;
+}
+
+.vc-notes-searcher-modal-user-notes-container
+*
+div:has(.vc-notes-searcher-modal-user-text-area) {
+ height: 67px;
+}
+
+.vc-notes-searcher-modal-user-text-area {
+ width: 100%;
+ height: 100%;
+}
+
+.vc-notes-searcher-modal-spinner::after {
+ content: "";
+ position: absolute;
+ width: 56px;
+ height: 56px;
+ border: 5px solid #fff;
+ border-radius: 50%;
+ display: inline-block;
+ box-sizing: border-box;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ animation: vc-notes-searcher-scale-up 1s linear infinite;
+}
+
+@keyframes vc-notes-searcher-scale-up {
+ 0% {
+ transform: translate(-50%, -50%) scale(0);
+ }
+
+ 60%,
+ 100% {
+ transform: translate(-50%, -50%) scale(1);
+ }
+}
+
+@keyframes vc-notes-searcher-pulse {
+ 0%,
+ 60%,
+ 100% {
+ transform: scale(0.9);
+ }
+
+ 80% {
+ transform: scale(1.1);
+ }
+}
diff --git a/src/plugins/notesSearcher/types.d.ts b/src/plugins/notesSearcher/types.d.ts
new file mode 100644
index 000000000..4506b0d34
--- /dev/null
+++ b/src/plugins/notesSearcher/types.d.ts
@@ -0,0 +1,37 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { FluxStore } from "@webpack/types";
+import { User as User$1 } from "discord-types/general";
+
+export type Note = {
+ loading: boolean;
+ note: string;
+};
+
+export type Notes = {
+ [userId: string]: Note;
+};
+
+export class NoteStore extends FluxStore {
+ getNotes(): Notes;
+ getNote(userId: string): Note;
+}
+
+export type User = User$1 & {
+ globalName?: string;
+};
+
+export type Dispatch = ReturnType>[1];
+
+export type UserCache = {
+ id: string;
+ globalName?: string;
+ username: string;
+ avatar: string;
+};
+
+export type UsersCache = Map;