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 + +
+