mirror of
https://github.com/Vendicated/Vencord.git
synced 2024-09-20 06:30:35 +00:00
new way of users caching & receive notes in READY
This commit is contained in:
parent
f31805c02f
commit
b4cc9844d5
6 changed files with 227 additions and 80 deletions
|
@ -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 <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
|
||||
|
@ -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"
|
||||
}
|
||||
</Button>
|
||||
<Button
|
||||
className={cl("cache-cache-missing")}
|
||||
size={Button.Sizes.NONE}
|
||||
color={Button.Colors.YELLOW}
|
||||
disabled={isRunning || cacheStatus === 0 || cacheStatus === NotesMap.size}
|
||||
disabled={isRunning || cacheStatus === 0 || cacheStatus >= notesLength}
|
||||
onClick={() => cacheUsers(true)}
|
||||
>
|
||||
Cache Missing Users
|
||||
|
@ -74,14 +76,14 @@ export default LazyComponent(() => React.memo(() => {
|
|||
<div className={cl("cache-status")}>
|
||||
{
|
||||
isRunning ? <LoadingSpinner />
|
||||
: cacheStatus === NotesMap.size ? <SuccessIcon />
|
||||
: cacheStatus >= notesLength ? <SuccessIcon />
|
||||
: cacheStatus === 0 ? <CrossIcon />
|
||||
: <ProblemIcon />
|
||||
}
|
||||
{
|
||||
cacheStatus === NotesMap.size ? "All users cached 👍"
|
||||
cacheStatus >= notesLength ? "Users are cached 👍"
|
||||
: cacheStatus === 0 ? "Users aren't cached 😔"
|
||||
: `${cacheStatus}/${NotesMap.size}`
|
||||
: `${cacheStatus}/${notesLength}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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 }: {
|
|||
<CachePopout />
|
||||
<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>
|
||||
}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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<string, string>();
|
||||
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<typeof useState<any>>[1]
|
||||
type Dispatch = ReturnType<typeof useState<any>>[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<string>();
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
14
src/plugins/notesSearcher/noteStore.d.ts
vendored
Normal file
14
src/plugins/notesSearcher/noteStore.d.ts
vendored
Normal file
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue