new way of users caching & receive notes in READY

This commit is contained in:
vishnyanetchereshnya 2024-06-21 05:59:40 +03:00 committed by GitHub
parent f31805c02f
commit b4cc9844d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 227 additions and 80 deletions

View file

@ -8,7 +8,7 @@ import { classNameFactory } from "@api/Styles";
import { LazyComponent } from "@utils/lazyReact"; import { LazyComponent } from "@utils/lazyReact";
import { Button, Popout, React, Text, useState } from "@webpack/common"; 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 { CrossIcon, ProblemIcon, SuccessIcon } from "./Icons";
import { LoadingSpinner } from "./LoadingSpinner"; import { LoadingSpinner } from "./LoadingSpinner";
@ -33,6 +33,8 @@ export default LazyComponent(() => React.memo(() => {
setCacheStatus, setCacheStatus,
}); });
const notesLength = Object.keys(getNotes()).length;
return <div className={cl("cache-container")}> return <div className={cl("cache-container")}>
<Text className={cl("cache-header")} variant="heading-lg/semibold"> <Text className={cl("cache-header")} variant="heading-lg/semibold">
Fetch the profile of all users to filter notes by global name or username 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()} onClick={() => cacheUsers()}
> >
{ {
cacheStatus === 0 ? "Cache Users" : "Re-Cache Users" cacheStatus === 10 ? "Cache Users" : "Re-Cache Users"
} }
</Button> </Button>
<Button <Button
className={cl("cache-cache-missing")} className={cl("cache-cache-missing")}
size={Button.Sizes.NONE} size={Button.Sizes.NONE}
color={Button.Colors.YELLOW} color={Button.Colors.YELLOW}
disabled={isRunning || cacheStatus === 0 || cacheStatus === NotesMap.size} disabled={isRunning || cacheStatus === 0 || cacheStatus >= notesLength}
onClick={() => cacheUsers(true)} onClick={() => cacheUsers(true)}
> >
Cache Missing Users Cache Missing Users
@ -74,14 +76,14 @@ export default LazyComponent(() => React.memo(() => {
<div className={cl("cache-status")}> <div className={cl("cache-status")}>
{ {
isRunning ? <LoadingSpinner /> isRunning ? <LoadingSpinner />
: cacheStatus === NotesMap.size ? <SuccessIcon /> : cacheStatus >= notesLength ? <SuccessIcon />
: cacheStatus === 0 ? <CrossIcon /> : cacheStatus === 0 ? <CrossIcon />
: <ProblemIcon /> : <ProblemIcon />
} }
{ {
cacheStatus === NotesMap.size ? "All users cached 👍" cacheStatus >= notesLength ? "Users are cached 👍"
: cacheStatus === 0 ? "Users aren't cached 😔" : cacheStatus === 0 ? "Users aren't cached 😔"
: `${cacheStatus}/${NotesMap.size}` : `${cacheStatus}/${notesLength}`
} }
</div> </div>
</div> </div>

View file

@ -11,7 +11,7 @@ import {
import { LazyComponent } from "@utils/react"; import { LazyComponent } from "@utils/react";
import { Button, React, RelationshipStore, Select, Text, TextInput, useCallback, useMemo, useReducer, useState } from "@webpack/common"; 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 CachePopout from "./CachePopout";
import NotesDataRow from "./NotesDataRow"; import NotesDataRow from "./NotesDataRow";
@ -54,8 +54,8 @@ export function NotesDataModal({ modalProps, close }: {
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status })); const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
const [usersNotesData, refreshNotesData] = useReducer(() => { const [usersNotesData, refreshNotesData] = useReducer(() => {
return Array.from(NotesMap); return Object.entries(getNotes());
}, Array.from(NotesMap)); }, Object.entries(getNotes()));
RefreshNotesDataEx = refreshNotesData; RefreshNotesDataEx = refreshNotesData;
@ -110,7 +110,6 @@ export function NotesDataModal({ modalProps, close }: {
<CachePopout /> <CachePopout />
<ModalCloseButton onClick={close} /> <ModalCloseButton onClick={close} />
</ModalHeader> </ModalHeader>
{
<div style={{ opacity: modalProps.transitionState === 1 ? "1" : "0" }} className={cl("content-container")}> <div style={{ opacity: modalProps.transitionState === 1 ? "1" : "0" }} className={cl("content-container")}>
{ {
modalProps.transitionState === 1 && modalProps.transitionState === 1 &&
@ -128,7 +127,6 @@ export function NotesDataModal({ modalProps, close }: {
</ModalContent> </ModalContent>
} }
</div> </div>
}
</ModalRoot> </ModalRoot>
); );
} }

View file

@ -10,7 +10,7 @@ import { copyWithToast } from "@utils/misc";
import { LazyComponent, useAwaiter } from "@utils/react"; import { LazyComponent, useAwaiter } from "@utils/react";
import { Alerts, Avatar, Button, ContextMenuApi, Menu, React, Text, TextArea, Tooltip, UserUtils, useState } from "@webpack/common"; 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 { DeleteIcon, PopupIcon, RefreshIcon, SaveIcon } from "./Icons";
import { LoadingSpinner } from "./LoadingSpinner"; import { LoadingSpinner } from "./LoadingSpinner";
@ -141,7 +141,6 @@ export default LazyComponent(() => React.memo(({ userId, userNotes: userNotesArg
size={Button.Sizes.NONE} size={Button.Sizes.NONE}
color={Button.Colors.GREEN} color={Button.Colors.GREEN}
onClick={() => { onClick={() => {
putNote(userId, userNotes);
updateNote(userId, userNotes); updateNote(userId, userNotes);
refreshNotesData(); refreshNotesData();
}} }}
@ -166,7 +165,6 @@ export default LazyComponent(() => React.memo(({ userId, userNotes: userNotesArg
confirmText: "Delete", confirmText: "Delete",
cancelText: "Cancel", cancelText: "Cancel",
onConfirm: () => { onConfirm: () => {
putNote(userId, "");
updateNote(userId, ""); updateNote(userId, "");
refreshNotesData(); refreshNotesData();
}, },

View file

@ -4,18 +4,25 @@
* SPDX-License-Identifier: GPL-3.0-or-later * 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";
export const NotesMap = new Map<string, string>(); import { refreshNotesData } from "./components/NotesDataModal";
import * as t from "./noteStore";
export const updateNote = (userId: string, note: string) => { let NoteStore: t.NoteStore;
if (!note || note === "")
NotesMap.delete(userId); waitForStore("NoteStore", s => NoteStore = s);
else
NotesMap.set(userId, note); 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({ RestAPI.put({
url: Constants.Endpoints.NOTE(userId), url: Constants.Endpoints.NOTE(userId),
body: { note }, 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: { const states: {
setRunning?: Dispatch; setRunning?: Dispatch;
@ -72,19 +79,86 @@ export const stopCacheProcess = () => {
cacheProcessNeedStop = true; cacheProcessNeedStop = true;
}; };
const stop = () => {
cacheProcessNeedStop = false;
isRunning = false;
states.setRunning?.(false);
};
export const cacheUsers = async (onlyMissing = false) => { export const cacheUsers = async (onlyMissing = false) => {
isRunning = true; isRunning = true;
states.setRunning?.(true); states.setRunning?.(true);
onlyMissing || usersCache.clear(); onlyMissing || usersCache.clear();
for (const userId of NotesMap.keys()) { const toRequest: string[] = [];
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,
});
continue;
}
toRequest.push(userId);
}
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) { if (cacheProcessNeedStop) {
cacheProcessNeedStop = false; stop();
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
break; break;
} }
if (onlyMissing && usersCache.get(userId)) continue; 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); const user = await fetchUser(userId);
@ -97,7 +171,22 @@ export const cacheUsers = async (onlyMissing = false) => {
states.setCacheStatus?.(usersCache.size); states.setCacheStatus?.(usersCache.size);
} }
} }
}
isRunning = false; stop();
states.setRunning?.(false); 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,
});
}
}; };

View file

@ -9,11 +9,11 @@ import "./styles.css";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Constants, RestAPI } from "@webpack/common"; import { FluxDispatcher } from "@webpack/common";
import { OpenNotesDataButton } from "./components/NotesDataButton"; import { OpenNotesDataButton } from "./components/NotesDataButton";
import { refreshNotesData } from "./components/NotesDataModal"; import { getNotes, onNoteUpdate } from "./data";
import { NotesMap, updateNote } from "./data"; import { Notes } from "./noteStore";
import settings from "./settings"; import settings from "./settings";
export default definePlugin({ export default definePlugin({
@ -22,21 +22,82 @@ export default definePlugin({
authors: [Devs.Vishnya], authors: [Devs.Vishnya],
settings, settings,
patches: [ patches: [
{
find: "noteRef",
replacement: {
match: /\i\.\i\.updateNote\((\i),(\i)\)/,
replace: "$self.updateNote($1, $2) || $self.refreshNotesData() || $&",
},
},
{ {
find: "toolbar:function", find: "toolbar:function",
replacement: { replacement: {
match: /(function \i\(\i\){)(.{1,200}toolbar.{1,100}mobileToolbar)/, 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; }) => { addToolBarButton: (children: { toolbar: React.ReactNode[] | React.ReactNode; }) => {
if (Array.isArray(children.toolbar)) if (Array.isArray(children.toolbar))
return children.toolbar.push( return children.toolbar.push(
@ -52,19 +113,4 @@ export default definePlugin({
children.toolbar, 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);
}
}
}); });

View 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;
}