From b4cc9844d56d405619350e64d6e2a2c8967528a7 Mon Sep 17 00:00:00 2001 From: vishnyanetchereshnya <151846235+vishnyanetchereshnya@users.noreply.github.com> Date: Fri, 21 Jun 2024 05:59:40 +0300 Subject: [PATCH] new way of users caching & receive notes in READY --- .../notesSearcher/components/CachePopout.tsx | 14 +- .../components/NotesDataModal.tsx | 42 +++--- .../notesSearcher/components/NotesDataRow.tsx | 4 +- src/plugins/notesSearcher/data.ts | 131 +++++++++++++++--- src/plugins/notesSearcher/index.tsx | 102 ++++++++++---- src/plugins/notesSearcher/noteStore.d.ts | 14 ++ 6 files changed, 227 insertions(+), 80 deletions(-) create mode 100644 src/plugins/notesSearcher/noteStore.d.ts diff --git a/src/plugins/notesSearcher/components/CachePopout.tsx b/src/plugins/notesSearcher/components/CachePopout.tsx index 6d9043645..5029959c7 100644 --- a/src/plugins/notesSearcher/components/CachePopout.tsx +++ b/src/plugins/notesSearcher/components/CachePopout.tsx @@ -8,7 +8,7 @@ import { classNameFactory } from "@api/Styles"; import { LazyComponent } from "@utils/lazyReact"; import { Button, Popout, React, Text, useState } from "@webpack/common"; -import { cacheUsers, getRunning, NotesMap, setupStates, stopCacheProcess, usersCache } from "../data"; +import { cacheUsers, getNotes, getRunning, setupStates, stopCacheProcess, usersCache } from "../data"; import { CrossIcon, ProblemIcon, SuccessIcon } from "./Icons"; import { LoadingSpinner } from "./LoadingSpinner"; @@ -33,6 +33,8 @@ export default LazyComponent(() => React.memo(() => { setCacheStatus, }); + const notesLength = Object.keys(getNotes()).length; + return
Fetch the profile of all users to filter notes by global name or username @@ -47,14 +49,14 @@ export default LazyComponent(() => React.memo(() => { onClick={() => cacheUsers()} > { - cacheStatus === 0 ? "Cache Users" : "Re-Cache Users" + cacheStatus === 10 ? "Cache Users" : "Re-Cache Users" }
diff --git a/src/plugins/notesSearcher/components/NotesDataModal.tsx b/src/plugins/notesSearcher/components/NotesDataModal.tsx index cbaff0633..4d7116d48 100644 --- a/src/plugins/notesSearcher/components/NotesDataModal.tsx +++ b/src/plugins/notesSearcher/components/NotesDataModal.tsx @@ -11,7 +11,7 @@ import { import { LazyComponent } from "@utils/react"; import { Button, React, RelationshipStore, Select, Text, TextInput, useCallback, useMemo, useReducer, useState } from "@webpack/common"; -import { NotesMap, usersCache } from "../data"; +import { getNotes, usersCache } from "../data"; import CachePopout from "./CachePopout"; import NotesDataRow from "./NotesDataRow"; @@ -54,8 +54,8 @@ export function NotesDataModal({ modalProps, close }: { const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status })); const [usersNotesData, refreshNotesData] = useReducer(() => { - return Array.from(NotesMap); - }, Array.from(NotesMap)); + return Object.entries(getNotes()); + }, Object.entries(getNotes())); RefreshNotesDataEx = refreshNotesData; @@ -110,25 +110,23 @@ export function NotesDataModal({ modalProps, close }: { - { -
- { - modalProps.transitionState === 1 && - - { - !visibleNotes.length ? : ( - - ) - } - - } -
- } +
+ { + modalProps.transitionState === 1 && + + { + !visibleNotes.length ? : ( + + ) + } + + } +
); } diff --git a/src/plugins/notesSearcher/components/NotesDataRow.tsx b/src/plugins/notesSearcher/components/NotesDataRow.tsx index 866e23bb5..16e21e5de 100644 --- a/src/plugins/notesSearcher/components/NotesDataRow.tsx +++ b/src/plugins/notesSearcher/components/NotesDataRow.tsx @@ -10,7 +10,7 @@ import { copyWithToast } from "@utils/misc"; import { LazyComponent, useAwaiter } from "@utils/react"; import { Alerts, Avatar, Button, ContextMenuApi, Menu, React, Text, TextArea, Tooltip, UserUtils, useState } from "@webpack/common"; -import { putNote, updateNote, usersCache } from "../data"; +import { updateNote, usersCache } from "../data"; import { DeleteIcon, PopupIcon, RefreshIcon, SaveIcon } from "./Icons"; import { LoadingSpinner } from "./LoadingSpinner"; @@ -141,7 +141,6 @@ export default LazyComponent(() => React.memo(({ userId, userNotes: userNotesArg size={Button.Sizes.NONE} color={Button.Colors.GREEN} onClick={() => { - putNote(userId, userNotes); updateNote(userId, userNotes); refreshNotesData(); }} @@ -166,7 +165,6 @@ export default LazyComponent(() => React.memo(({ userId, userNotes: userNotesArg confirmText: "Delete", cancelText: "Cancel", onConfirm: () => { - putNote(userId, ""); updateNote(userId, ""); refreshNotesData(); }, diff --git a/src/plugins/notesSearcher/data.ts b/src/plugins/notesSearcher/data.ts index 89fdbf04a..95206b598 100644 --- a/src/plugins/notesSearcher/data.ts +++ b/src/plugins/notesSearcher/data.ts @@ -4,18 +4,25 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Constants, RestAPI, UserUtils, useState } from "@webpack/common"; +import { Constants, FluxDispatcher, GuildStore, RestAPI, SnowflakeUtils, UserStore, UserUtils, useState } from "@webpack/common"; +import { waitForStore } from "webpack/common/internal"; + +import { refreshNotesData } from "./components/NotesDataModal"; +import * as t from "./noteStore"; -export const NotesMap = new Map(); +let NoteStore: t.NoteStore; -export const updateNote = (userId: string, note: string) => { - if (!note || note === "") - NotesMap.delete(userId); - else - NotesMap.set(userId, note); +waitForStore("NoteStore", s => NoteStore = s); + +export const getNotes = () => { + return NoteStore.getNotes(); }; -export const putNote = (userId: string, note: string | null) => { +export const onNoteUpdate = () => { + refreshNotesData(); +}; + +export const updateNote = (userId: string, note: string | null) => { RestAPI.put({ url: Constants.Endpoints.NOTE(userId), body: { note }, @@ -42,7 +49,7 @@ const fetchUser = async (userId: string) => { } }; -type Dispatch = ReturnType>[1] +type Dispatch = ReturnType>[1]; const states: { setRunning?: Dispatch; @@ -72,32 +79,114 @@ export const stopCacheProcess = () => { cacheProcessNeedStop = true; }; +const stop = () => { + cacheProcessNeedStop = false; + isRunning = false; + states.setRunning?.(false); +}; + 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; - } + const toRequest: string[] = []; - if (onlyMissing && usersCache.get(userId)) continue; - - const user = await fetchUser(userId); + for (const userId of Object.keys(getNotes())) { + const user = UserStore.getUser(userId); if (user) { usersCache.set(user.id, { globalName: (user as any).globalName, username: user.username, }); - - states.setCacheStatus?.(usersCache.size); + continue; } + + toRequest.push(userId); } - isRunning = false; - states.setRunning?.(false); + if (usersCache.size >= Object.keys(getNotes()).length) { + stop(); + states.setCacheStatus?.(usersCache.size); + return; + } + + states.setCacheStatus?.(usersCache.size); + + 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) { + if (cacheProcessNeedStop) { + stop(); + FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback); + break; + } + + const { nonce, members } = chunk; + + if (nonce !== sentNonce) { + return; + } + + members.forEach(member => { + if (processed.has(member.user.id)) return; + + processed.add(member.user.id); + + usersCache.set(member.id, { + globalName: (member as any).globalName, + username: member.username, + }); + + states.setCacheStatus?.(usersCache.size); + }); + + if (--count === 0) { + const userIds = Object.keys(getNotes()); + + if (usersCache.size !== userIds.length) { + for (const userId of userIds) { + if (cacheProcessNeedStop) { + stop(); + FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback); + break; + } + + const user = await fetchUser(userId); + + if (user) { + usersCache.set(user.id, { + globalName: (user as any).globalName, + username: user.username, + }); + + states.setCacheStatus?.(usersCache.size); + } + } + } + + stop(); + FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback); + } + } + }; + + 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 index c0defe465..f8b51c510 100644 --- a/src/plugins/notesSearcher/index.tsx +++ b/src/plugins/notesSearcher/index.tsx @@ -9,34 +9,95 @@ import "./styles.css"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; -import { Constants, RestAPI } from "@webpack/common"; +import { FluxDispatcher } from "@webpack/common"; import { OpenNotesDataButton } from "./components/NotesDataButton"; -import { refreshNotesData } from "./components/NotesDataModal"; -import { NotesMap, updateNote } from "./data"; +import { getNotes, onNoteUpdate } from "./data"; +import { Notes } from "./noteStore"; import settings from "./settings"; - + 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: "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" - } + 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 + { + find: '="NoteStore",', + replacement: { + match: /CONNECTION_OPEN:_,OVERLAY_INITIALIZE:_,/, + 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); + }, + stop: () => { + FluxDispatcher.unsubscribe("USER_NOTE_UPDATE", onNoteUpdate); + }, + + ready: ({ notes }: { notes: Notes; }) => { + const notesFromStore = getNotes(); + + for (const userId of Object.keys(notesFromStore)) { + delete notesFromStore[userId]; + } + + Object.assign(notesFromStore, notes); + }, + addToolBarButton: (children: { toolbar: React.ReactNode[] | React.ReactNode; }) => { if (Array.isArray(children.toolbar)) return children.toolbar.push( @@ -52,19 +113,4 @@ export default definePlugin({ children.toolbar, ]; }, - - updateNote, - refreshNotesData, - - start: async () => { - const result = await RestAPI.get({ url: Constants.Endpoints.NOTES }); - - const userNotes: { [userId: string]: string; } | undefined = result.body; - - if (!userNotes) return; - - for (const [userId, note] of Object.entries(userNotes)) { - NotesMap.set(userId, note); - } - } }); diff --git a/src/plugins/notesSearcher/noteStore.d.ts b/src/plugins/notesSearcher/noteStore.d.ts new file mode 100644 index 000000000..c40bc4fa8 --- /dev/null +++ b/src/plugins/notesSearcher/noteStore.d.ts @@ -0,0 +1,14 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { FluxStore } from "@webpack/types"; + +export type Notes = { [userId: string]: string; }; + +export class NoteStore extends FluxStore { + getNotes(): Notes; + getNote(userId: string): string | null; +}