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

View file

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

View file

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

View file

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

View file

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

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