[Plugin] NotesSearcher

Allows you to open modal with all of your notes and search throught them by UserID, Note text and Global/Username if user is cached
This commit is contained in:
vishnyanetchereshnya 2024-06-20 23:47:15 +03:00 committed by GitHub
parent a43d5d595a
commit a5be1873ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1108 additions and 0 deletions

View file

@ -0,0 +1,8 @@
# NotesSearcher
Allows you to open modal with all of your notes and search throught them by UserID, Note text and Global/Username if user is cached
## Preview
![preview](https://i.imgur.com/FJl4W13.png)
![preview](https://i.imgur.com/nCyP8Uf.png)

View file

@ -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 (
<svg stroke="currentColor" width="24" height="24" viewBox="1 1 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" d="M10.0002 4H7.2002C6.08009 4 5.51962 4 5.0918 4.21799C4.71547 4.40973 4.40973 4.71547 4.21799 5.0918C4 5.51962 4 6.08009 4 7.2002V16.8002C4 17.9203 4 18.4801 4.21799 18.9079C4.40973 19.2842 4.71547 19.5905 5.0918 19.7822C5.5192 20 6.07899 20 7.19691 20H16.8031C17.921 20 18.48 20 18.9074 19.7822C19.2837 19.5905 19.5905 19.2839 19.7822 18.9076C20 18.4802 20 17.921 20 16.8031V14M16 5L10 11V14H13L19 8M16 5L19 2L22 5L19 8M16 5L19 8" />
</svg>
);
}));
export const SaveIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32" height="32" viewBox="-4 -4 32 32">
<path fill="#fff" fill-rule="evenodd" clip-rule="evenodd" d="M18.1716 1C18.702 1 19.2107 1.21071 19.5858 1.58579L22.4142 4.41421C22.7893 4.78929 23 5.29799 23 5.82843V20C23 21.6569 21.6569 23 20 23H4C2.34315 23 1 21.6569 1 20V4C1 2.34315 2.34315 1 4 1H18.1716ZM4 3C3.44772 3 3 3.44772 3 4V20C3 20.5523 3.44772 21 4 21L5 21L5 15C5 13.3431 6.34315 12 8 12L16 12C17.6569 12 19 13.3431 19 15V21H20C20.5523 21 21 20.5523 21 20V6.82843C21 6.29799 20.7893 5.78929 20.4142 5.41421L18.5858 3.58579C18.2107 3.21071 17.702 3 17.1716 3H17V5C17 6.65685 15.6569 8 14 8H10C8.34315 8 7 6.65685 7 5V3H4ZM17 21V15C17 14.4477 16.5523 14 16 14L8 14C7.44772 14 7 14.4477 7 15L7 21L17 21ZM9 3H15V5C15 5.55228 14.5523 6 14 6H10C9.44772 6 9 5.55228 9 5V3Z" />
</svg>
);
}));
export const DeleteIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32" height="32" viewBox="-4 -4 32 32">
<path fill="#fff" d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-3.5l-1-1zM18 7H6v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7z" />
</svg>
);
}));
export const RefreshIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32" height="32" viewBox="-4 -4 32 32" fill="none">
<path stroke-linejoin="round" stroke-linecap="round" stroke="#fff" stroke-width="2" d="M21 3V8M21 8H16M21 8L18 5.29168C16.4077 3.86656 14.3051 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.2832 21 19.8675 18.008 20.777 14" />
</svg>
);
}));
export const PopupIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32" height="32" viewBox="-4 -4 32 32">
<path fill="#fff" d="M10 5V3H5.375C4.06519 3 3 4.06519 3 5.375V18.625C3 19.936 4.06519 21 5.375 21H18.625C19.936 21 21 19.936 21 18.625V14H19V19H5V5H10Z" />
<path fill="#fff" d="M21 2.99902H14V4.99902H17.586L9.29297 13.292L10.707 14.706L19 6.41302V9.99902H21V2.99902Z" />
</svg>
);
}));
export const CrossIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32px" height="32px" viewBox="-3.2 -3.2 38.40 38.40">
<rect fill="#dc3545" x="-3.2" y="-3.2" width="38.40" height="38.40" rx="19.2" />
<path fill="#fff" d="M18.8,16l5.5-5.5c0.8-0.8,0.8-2,0-2.8l0,0C24,7.3,23.5,7,23,7c-0.5,0-1,0.2-1.4,0.6L16,13.2l-5.5-5.5 c-0.8-0.8-2.1-0.8-2.8,0C7.3,8,7,8.5,7,9.1s0.2,1,0.6,1.4l5.5,5.5l-5.5,5.5C7.3,21.9,7,22.4,7,23c0,0.5,0.2,1,0.6,1.4 C8,24.8,8.5,25,9,25c0.5,0,1-0.2,1.4-0.6l5.5-5.5l5.5,5.5c0.8,0.8,2.1,0.8,2.8,0c0.8-0.8,0.8-2.1,0-2.8L18.8,16z" />
</svg>
);
}));
export const ProblemIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32px" height="32px" viewBox="0 0 1024 1024">
<rect fill="#fbbd04" x="0" y="0" width="1024" height="1024" rx="512" />
<path fill="#fff" d="M512 254.08a140.16 140.16 0 0 0-140.672 139.392 32.128 32.128 0 0 0 64.32 0c0-42.112 33.536-75.136 76.352-75.136 42.112 0 76.352 34.56 76.352 76.992 0 16-22.912 38.976-43.2 59.2-30.592 30.592-65.28 65.28-65.28 111.744v45.888a32.128 32.128 0 1 0 64.256 0v-45.888c0-19.84 23.68-43.52 46.464-66.304 29.056-29.056 62.08-62.016 62.08-104.64A141.12 141.12 0 0 0 512 254.08z m-48.192 500.928a48.192 48.192 0 1 0 96.384 0 48.192 48.192 0 0 0-96.384 0z" />
</svg>
);
}));
export const SuccessIcon = LazyComponent(() => React.memo(() => {
return (
<svg width="32px" height="32px" viewBox="0.55 2.3 15.834375 15.834375">
<circle fill="#fff" cx="8.5" cy="10.25" r="6.5" />
<path fill="#28a745" d="M16.417 10.283A7.917 7.917 0 1 1 8.5 2.366a7.916 7.916 0 0 1 7.917 7.917zm-4.105-4.498a.791.791 0 0 0-1.082.29l-3.828 6.63-1.733-2.08a.791.791 0 1 0-1.216 1.014l2.459 2.952a.792.792 0 0 0 .608.285.83.83 0 0 0 .068-.003.791.791 0 0 0 .618-.393L12.6 6.866a.791.791 0 0 0-.29-1.081z" />
</svg>
);
}));

View file

@ -0,0 +1,27 @@
/*
* 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 { filters, find } from "@webpack";
import { NotesDataIcon } from "./Icons";
import { openNotesDataModal } from "./NotesDataModal";
const HeaderBarIcon = LazyComponent(() => {
const filter = filters.byCode(".HEADER_BAR_BADGE");
return find(m => m.Icon && filter(m.Icon)).Icon;
});
export function OpenNotesDataButton() {
return (
<HeaderBarIcon
className="vc-notes-searcher-toolbox-button"
onClick={() => openNotesDataModal()}
tooltip={"Open Notes Data"}
icon={NotesDataIcon}
/>
);
}

View file

@ -0,0 +1,514 @@
/*
* 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 {
closeModal, ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, openModal
} from "@utils/modal";
import { LazyComponent, useAwaiter } from "@utils/react";
import { Alerts, Avatar, Button, ContextMenuApi, Menu, Popout, React, RelationshipStore, Select, Text, TextArea, TextInput, Tooltip, useCallback, useMemo, useReducer, UserUtils, useState } from "@webpack/common";
import { cacheUsers, getRunning, NotesMap, putNote, setupStates, stopCacheProcess, updateNote, usersCache } from "../data";
import { CrossIcon, DeleteIcon, PopupIcon, ProblemIcon, RefreshIcon, SaveIcon, SuccessIcon } from "./Icons";
const cl = classNameFactory("vc-notes-searcher-modal-");
const enum SearchStatus {
ALL,
FRIENDS,
BLOCKED,
}
const filterByUserDetails = (userId: string, query: string) => {
if (userId.includes(query)) return true;
const user = usersCache.get(userId);
if (!user) return false;
return user.globalName?.includes(query) || user.username.includes(query);
};
// looks like a shit but I didn't know better way to do it
let RefreshNotesDataEx: () => void | undefined;
export const refreshNotesData = () => {
if (!RefreshNotesDataEx) return;
RefreshNotesDataEx();
};
export function NotesDataModal({ modalProps, close }: {
modalProps: ModalProps;
close(): void;
}) {
const [shouldShow, setShouldShow] = useState(false);
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 Array.from(NotesMap);
}, Array.from(NotesMap));
RefreshNotesDataEx = refreshNotesData;
const filteredNotes = useMemo(() => {
const { query, status } = searchValue;
if (query === "" && status === SearchStatus.ALL) {
return usersNotesData;
}
return usersNotesData
.filter(([userId, userNotes]) => {
return (
status === SearchStatus.FRIENDS ?
RelationshipStore.isFriend(userId) &&
(
query === "" ||
(
filterByUserDetails(userId, query) ||
userNotes.toLowerCase().includes(query.toLowerCase())
)
)
:
status === SearchStatus.BLOCKED ?
RelationshipStore.isBlocked(userId) &&
(
query === "" ||
(
filterByUserDetails(userId, query) ||
userNotes.toLowerCase().includes(query.toLowerCase())
)
)
:
query === "" ||
(
filterByUserDetails(userId, query) ||
userNotes.toLowerCase().includes(query.toLowerCase())
)
);
});
}, [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 (
<ModalRoot className={cl("root")} {...modalProps}>
<ModalHeader className={cl("header")}>
<Text className={cl("header-text")} variant="heading-lg/semibold">Notes Data</Text>
<TextInput className={cl("header-input")} value={searchValue.query} onChange={onSearch} placeholder="Filter Notes (ID/Notes and Global/Username if cached)" />
<div className={cl("header-user-type")}>
<Select
options={[
{ label: "Show All", value: SearchStatus.ALL, default: true },
{ label: "Show Friends", value: SearchStatus.FRIENDS },
{ label: "Show Blocked", value: SearchStatus.BLOCKED },
]}
serialize={String}
select={onStatusChange}
isSelected={v => v === searchValue.status}
closeOnSelect={true}
/>
</div>
<Popout
animation={Popout.Animation.SCALE}
align="center"
position="bottom"
shouldShow={shouldShow}
onRequestClose={() => setShouldShow(false)}
renderPopout={() => {
const [isRunning, setRunning] = useState(getRunning);
const [cacheStatus, setCacheStatus] = useState(usersCache.size);
setupStates({
setRunning,
setCacheStatus,
});
return <div className={cl("cache-container")}>
<Text className={cl("cache-header")} variant="heading-lg/semibold">
Fetch the profile of all users to filter notes by global name or username
</Text>
<div>
<div className={cl("cache-buttons")}>
<Button
className={cl("cache-cache")}
size={Button.Sizes.NONE}
color={Button.Colors.GREEN}
disabled={isRunning}
onClick={() => cacheUsers()}
>
{
cacheStatus === 0 ? "Cache Users" : "Re-Cache Users"
}
</Button>
<Button
className={cl("cache-cache-missing")}
size={Button.Sizes.NONE}
color={Button.Colors.YELLOW}
disabled={isRunning || cacheStatus === 0 || cacheStatus === NotesMap.size}
onClick={() => cacheUsers(true)}
>
Cache Missing Users
</Button>
<Button
className={cl("cache-stop")}
size={Button.Sizes.NONE}
color={Button.Colors.RED}
disabled={!isRunning}
onClick={() => {
stopCacheProcess();
}}
>
Stop
</Button>
</div>
<div className={cl("cache-status")}>
{
isRunning ? <LoadingSpinner />
: cacheStatus === NotesMap.size ? <SuccessIcon />
: cacheStatus === 0 ? <CrossIcon />
: <ProblemIcon />
}
{
cacheStatus === NotesMap.size ? "All users cached 👍"
: cacheStatus === 0 ? "Users didn't cached 😔"
: `${cacheStatus}/${NotesMap.size}`
}
</div>
</div>
{/* <Text className={cl("cache-warning")} variant="heading-md/normal"> */}
<Text className={cl("cache-warning")} variant="text-md/normal">
Please note that during this process Discord may not properly load some content, such as messages, images or user profiles
</Text>
<Text className={cl("cache-footer")} variant="text-md/normal">
You can turn on caching of all users on startup in plugin settings
</Text>
</div>;
}}
>
{
(_, { isShown }) =>
<Button
className={cl("header-cache")}
size={Button.Sizes.NONE}
color={Button.Colors.PRIMARY}
onClick={() => setShouldShow(!isShown)}
>
Cache
</Button>
}
</Popout>
<ModalCloseButton onClick={close} />
</ModalHeader>
{
<div style={{ opacity: modalProps.transitionState === 1 ? "1" : "0" }} className={cl("content-container")}>
{
modalProps.transitionState === 1 &&
<ModalContent className={cl("content")}>
{
!visibleNotes.length ? NoNotes() : (
<NotesDataContent
visibleNotes={visibleNotes}
canLoadMore={canLoadMore}
loadMore={loadMore}
refreshNotesData={refreshNotesData}
/>
)
}
</ModalContent>
}
</div>
}
</ModalRoot>
);
}
function NotesDataContentFactory({ visibleNotes, canLoadMore, loadMore, refreshNotesData }: {
visibleNotes: [string, string][];
canLoadMore: boolean;
loadMore(): void;
refreshNotesData(): void;
}) {
if (!visibleNotes.length)
return NoNotes();
return (
<div className={cl("content-inner")}>
{
visibleNotes
.map(([userId, userNotes]) => (
<NotesDataRow
key={userId}
userId={userId}
userNotes={userNotes}
refreshNotesData={refreshNotesData}
/>
))
}
{
canLoadMore &&
<Button
className={cl("load-more")}
size={Button.Sizes.NONE}
onClick={() => loadMore()}
>
Load More
</Button>
}
</div>
);
}
const NotesDataContent = LazyComponent(() => React.memo(NotesDataContentFactory));
function NoNotes() {
return (
<div className={cl("no-notes")} style={{ textAlign: "center" }}>
<Text variant="text-lg/normal">
No Notes.
</Text>
</div>
);
}
type UserInfo = {
id: string;
globalName: string;
username: string;
avatar: string;
};
function NotesDataRow({ userId, userNotes: userNotesArg, refreshNotesData }: {
userId: string;
userNotes: string;
refreshNotesData(): void;
}) {
const awaitedResult = useAwaiter(async () => {
const user = await UserUtils.getUser(userId);
usersCache.set(userId, {
globalName: (user as any).globalName ?? user.username,
username: user.username,
});
return {
id: userId,
globalName: (user as any).globalName ?? user.username,
username: user.username,
avatar: user.getAvatarURL(void 0, void 0, false),
} as UserInfo;
});
let userInfo = awaitedResult[0];
const pending = awaitedResult[2];
userInfo ??= {
id: userId,
globalName: pending ? "Loading..." : "Unable to load",
username: pending ? "Loading..." : "Unable to load",
avatar: "https://discord.com/assets/0048cbfdd0b3ef186d22.png",
} as const;
const [userNotes, setUserNotes] = useState(userNotesArg);
return (
<div
className={cl("user")}
onContextMenu={event => {
ContextMenuApi.openContextMenu(event, () =>
<Menu.Menu
navId={cl("user-context-menu")}
onClose={ContextMenuApi.closeContextMenu}
aria-label="User Notes Data"
>
<Menu.MenuItem
id={cl("open-user-profile")}
label="Open User Profile"
action={() => openUserProfile(userId)}
/>
<Menu.MenuItem
id={cl("open-user-chat")}
label="Open User Chat"
action={() => openPrivateChannel(userId)}
/>
<Menu.MenuItem
id={cl("copy-user-id")}
label="Copy ID"
action={() => copyWithToast(userInfo!.id)}
/>
{
!pending &&
(
<>
<Menu.MenuItem
id={cl("copy-user-globalname")}
label="Copy Global Name"
action={() => copyWithToast(userInfo!.globalName)}
/>
<Menu.MenuItem
id={cl("copy-user-username")}
label="Copy Username"
action={() => copyWithToast(userInfo!.username)}
/>
<Menu.MenuItem
id={cl("copy-user-avatar")}
label="Copy Avatar URL"
action={() => copyWithToast(userInfo!.avatar)}
/>
</>
)
}
<Menu.MenuItem
id={cl("copy-user-notes")}
label="Copy Notes"
action={() => copyWithToast(userNotes)}
/>
</Menu.Menu>
);
}}
>
{
pending ? <LoadingSpinner /> :
<Avatar
className={cl("user-avatar")}
size="SIZE_56"
src={userInfo.avatar}
/>
}
<div className={cl("user-info")}>
<Text className={cl("user-info-globalname")} variant="text-lg/bold">{userInfo.globalName}</Text>
<Text className={cl("user-info-username")} variant="text-md/normal">{userInfo.username}</Text>
<Text className={cl("user-info-id")} variant="text-md/normal">{userInfo.id}</Text>
</div>
<div className={cl("user-notes-container")}>
<TextArea
className={cl("user-text-area")}
placeholder="Click to add a note"
value={userNotes}
onChange={setUserNotes}
spellCheck={false}
/>
<div className={cl("user-actions")}>
<Tooltip text={"Save"}>
{({ onMouseLeave, onMouseEnter }) => (
<Button
className={cl("user-actions-save")}
size={Button.Sizes.NONE}
color={Button.Colors.GREEN}
onClick={() => {
putNote(userId, userNotes);
updateNote(userId, userNotes);
refreshNotesData();
}}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
>
<SaveIcon />
</Button>
)}
</Tooltip>
<Tooltip text={"Delete"}>
{({ onMouseLeave, onMouseEnter }) => (
<Button
className={cl("user-actions-delete")}
size={Button.Sizes.NONE}
color={Button.Colors.RED}
onClick={() => {
Alerts.show({
title: "Delete Notes",
body: `Are you sure you want to delete notes for ${pending ? userId : `${userInfo!.globalName} (${userId})`}?`,
confirmColor: Button.Colors.RED,
confirmText: "Delete",
cancelText: "Cancel",
onConfirm: () => {
putNote(userId, "");
updateNote(userId, userNotes);
refreshNotesData();
},
});
}}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
>
<DeleteIcon />
</Button>
)}
</Tooltip>
<Tooltip text={"Undo text area changes"}>
{({ onMouseLeave, onMouseEnter }) => (
<Button
className={cl("user-actions-refresh")}
size={Button.Sizes.NONE}
color={Button.Colors.LINK}
onClick={() => setUserNotes(userNotesArg)}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
>
<RefreshIcon />
</Button>
)}
</Tooltip>
<Tooltip text={"Open User Profile"}>
{({ onMouseLeave, onMouseEnter }) => (
<Button
className={cl("user-actions-popup")}
size={Button.Sizes.NONE}
color={Button.Colors.PRIMARY}
onClick={async () => {
openUserProfile(userId);
}}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
>
<PopupIcon />
</Button>
)}
</Tooltip>
</div>
</div>
</div>
);
}
const LoadingSpinner = LazyComponent(() => React.memo(() => {
return (
<div className={cl("loading-container")}>
<span className={cl("loading")} />
</div>
);
}));
const CacheSpinner = LazyComponent(() => React.memo(() => {
return (
<div className={cl("caching-container")}>
<span className={cl("caching")} />
</div>
);
}));
export const openNotesDataModal = async () => {
const key = openModal(modalProps => (
<NotesDataModal
modalProps={modalProps}
close={() => closeModal(key)}
/>
));
};

View file

@ -0,0 +1,101 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Constants, RestAPI, UserUtils, useState } from "@webpack/common";
export const NotesMap = new Map<string, string>();
export const updateNote = (userId: string, note: string) => {
if (!note || note === "")
NotesMap.delete(userId);
else
NotesMap.set(userId, note);
};
export const putNote = (userId: string, note: string | null) => {
RestAPI.put({
url: Constants.Endpoints.NOTE(userId),
body: { note },
oldFormErrors: true
});
};
export const usersCache = new Map<string, {
globalName?: string,
username: string;
}>();
const fetchUser = async (userId: string) => {
for (let _ = 0; _ < 10; _++) {
try {
return await UserUtils.getUser(userId);
} catch (error: any) {
const wait = error?.body?.retry_after;
if (!wait) break;
await new Promise(resolve => setTimeout(resolve, wait * 1000 + 50));
}
}
};
const states: {
setRunning?: ReturnType<typeof useState<any>>[1];
setCacheStatus?: ReturnType<typeof useState<any>>[1],
} = {};
export const setupStates = ({
setRunning,
setCacheStatus,
}: {
setRunning: ReturnType<typeof useState<any>>[1],
setCacheStatus: ReturnType<typeof useState<any>>[1],
}) => {
states.setRunning = setRunning;
states.setCacheStatus = setCacheStatus;
};
let isRunning = false;
export const getRunning = () => {
return isRunning;
};
let cacheProcessNeedStop = false;
export const stopCacheProcess = () => {
cacheProcessNeedStop = true;
};
export const cacheUsers = async (onlyMissing = false) => {
isRunning = true;
states.setRunning?.(true);
onlyMissing || usersCache.clear();
for (const userId of NotesMap.keys()) {
if (cacheProcessNeedStop) {
cacheProcessNeedStop = false;
break;
}
if (onlyMissing && usersCache.get(userId)) continue;
const user = await fetchUser(userId);
if (user) {
usersCache.set(user.id, {
globalName: (user as any).globalName,
username: user.username,
});
states.setCacheStatus?.(usersCache.size);
}
}
isRunning = false;
states.setRunning?.(false);
};

View file

@ -0,0 +1,70 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { RestAPI } from "@webpack/common";
import { OpenNotesDataButton } from "./components/NotesDataButton";
import { refreshNotesData } from "./components/NotesDataModal";
import { NotesMap, updateNote } from "./data";
import settings from "./settings";
export default definePlugin({
name: "NotesSearcher",
description: "Allows you to open modal with all of your notes and search throught them by UserID, Note text and Global/Username if user is cached",
authors: [Devs.Vishnya],
settings,
patches: [
{
find: "noteRef",
replacement: {
match: /\i\.\i\.updateNote\((\i),(\i)\)/,
replace: "$self.updateNote($1, $2) || $self.refreshNotesData() || $&",
},
},
{
find: "toolbar:function",
replacement: {
match: /(function \i\(\i\){)(.{1,200}toolbar.{1,100}mobileToolbar)/,
replace: "$1$self.addToolBarButton(arguments[0]);$2"
}
},
],
addToolBarButton: (children: { toolbar: React.ReactNode[] | React.ReactNode; }) => {
if (Array.isArray(children.toolbar))
return children.toolbar.push(
<ErrorBoundary noop={true}>
<OpenNotesDataButton />
</ErrorBoundary>
);
children.toolbar = [
<ErrorBoundary noop={true}>
<OpenNotesDataButton />
</ErrorBoundary>,
children.toolbar,
];
},
updateNote,
refreshNotesData,
start: async () => {
const result = await RestAPI.get({ url: "/users/@me/notes" });
const userNotes: { [userId: string]: string; } | undefined = result.body;
if (!userNotes) return;
for (const [userId, note] of Object.entries(userNotes)) {
NotesMap.set(userId, note);
}
}
});

View file

@ -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",
},
});

View file

@ -0,0 +1,296 @@
/* Part of Cache Popout Data. css linter is awesome */
.vc-notes-searcher-modal-cache-status svg,
.vc-notes-searcher-modal-cache-status div,
.vc-notes-searcher-modal-cache-status div span,
.vc-notes-searcher-modal-cache-status div span::after {
width: 40px;
height: 40px;
}
/* Open "Notes Data" toolbox button */
.vc-notes-searcher-toolbox-button svg {
color: var(--interactive-normal);
}
/* Part of Notes Modal Data. css linter is awesome */
.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);
}
/* Notes Data Modal */
.vc-notes-searcher-modal-root {
min-height: 75vh;
max-height: 75vh;
min-width: 70vw;
max-width: 70vw;
}
.vc-notes-searcher-modal-header-text {
white-space: nowrap;
width: fit-content;
margin-right: 16px;
}
.vc-notes-searcher-modal-header-input {
width: 100%;
margin-right: 16px;
}
.vc-notes-searcher-modal-header-user-type {
min-width: 160px;
margin-right: 16px;
}
.vc-notes-searcher-modal-header-cache {
width: fit-content;
height: 100%;
margin-right: 16px;
}
.vc-notes-searcher-modal-content-container {
overflow: hidden;
height: 100%;
}
.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 {
padding-top: 16px;
height: fit-content;
}
.vc-notes-searcher-modal-user-actions * {
width: 32px;
height: 32px;
}
.vc-notes-searcher-modal-content-inner > *:not(:last-child) {
margin-bottom: 8px;
}
.vc-notes-searcher-modal-load-more {
margin-top: 16px;
width: 100%;
height: 32px;
}
.vc-notes-searcher-modal-no-notes {
display: grid;
place-content: center;
height: 100%;
}
.vc-notes-searcher-modal-user {
width: 100%;
height: 80px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
background-color: var(--background-secondary);
border-radius: 12px;
box-sizing: border-box;
}
.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-info {
min-width: 50px;
max-width: 275px;
width: 100%;
}
.vc-notes-searcher-modal-user-info > div {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.vc-notes-searcher-modal-user-info-globalname {
color: #fff;
}
.vc-notes-searcher-modal-user-info-username {
color: #d3d3d3;
}
.vc-notes-searcher-modal-user-info-id {
color: #989898;
}
.vc-notes-searcher-modal-user-notes-container {
display: grid;
grid-template-columns: calc(100% - 86px) min-content;
align-items: center;
justify-content: flex-end;
flex-grow: 1;
padding-right: 8px;
gap: 8px;
}
.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-user-actions {
display: grid;
grid-template-columns: auto auto;
grid-template-rows: auto auto;
gap: 3px;
aspect-ratio: 1 / 1;
height: auto;
box-sizing: border-box;
overflow: visible !important;
}
/* Loading Spinner */
.vc-notes-searcher-modal-loading-container {
/* aspect-ratio: 1 / 1; */
width: 56px;
height: 56px;
margin: 12px;
}
.vc-notes-searcher-modal-loading {
width: 56px;
height: 56px;
border: 5px solid #fff;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
position: relative;
animation: vc-user-notes-pulse 1s linear infinite;
}
.vc-notes-searcher-modal-loading::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-user-notes-scale-up 1s linear infinite;
}
@keyframes vc-user-notes-scale-up {
0% {
transform: translate(-50%, -50%) scale(0);
}
60%,
100% {
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes vc-user-notes-pulse {
0%,
60%,
100% {
transform: scale(0.9);
}
80% {
transform: scale(1.1);
}
}
.vc-notes-searcher-modal-cache-container {
width: 25vw;
background-color: var(--modal-background);
border-radius: 4px;
padding: 12px;
box-shadow: var(--legacy-elevation-border), var(--legacy-elevation-high);
}
.vc-notes-searcher-modal-cache-header {
margin-bottom: 12px;
}
.vc-notes-searcher-modal-cache-warning {
margin-top: 12px;
border-radius: 4px;
background-color: #e7828430;
border: 1px solid #e78284;
padding: 8px;
}
.vc-notes-searcher-modal-cache-footer {
margin-top: 12px;
border-radius: 4px;
background-color: #00ff404a;
border: 1px solid #2fd334;
padding: 8px;
}
.vc-notes-searcher-modal-cache-container > div:nth-child(2) {
display: grid;
grid-template-columns: min-content auto;
}
.vc-notes-searcher-modal-cache-buttons {
width: fit-content;
display: grid;
row-gap: 6px;
}
.vc-notes-searcher-modal-cache-buttons > button {
height: 32px;
}
.vc-notes-searcher-modal-cache-status {
display: grid;
align-items: center;
color: var(--text-normal);
font-size: 20px;
line-height: 24px;
text-align: center;
padding: 0 12px;
}
.vc-notes-searcher-modal-cache-status :nth-child(1) {
margin: 0 auto;
}