Lint functional hooks

This commit is contained in:
Zerebos 2023-03-19 22:23:11 -04:00
parent c2d1e4505f
commit 7b58be079d
26 changed files with 153 additions and 129 deletions

View File

@ -21,6 +21,7 @@
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"eslint": "^8.23.0", "eslint": "^8.23.0",
"eslint-plugin-react": "^7.31.6", "eslint-plugin-react": "^7.31.6",
"eslint-plugin-react-hooks": "^4.6.0",
"mocha": "^10.0.0", "mocha": "^10.0.0",
"webpack": "^5.74.0", "webpack": "^5.74.0",
"webpack-cli": "^4.10.0" "webpack-cli": "^4.10.0"

View File

@ -8,6 +8,7 @@ importers:
dotenv: ^16.0.3 dotenv: ^16.0.3
eslint: ^8.23.0 eslint: ^8.23.0
eslint-plugin-react: ^7.31.6 eslint-plugin-react: ^7.31.6
eslint-plugin-react-hooks: ^4.6.0
mocha: ^10.0.0 mocha: ^10.0.0
webpack: ^5.74.0 webpack: ^5.74.0
webpack-cli: ^4.10.0 webpack-cli: ^4.10.0
@ -16,6 +17,7 @@ importers:
dotenv: 16.0.3 dotenv: 16.0.3
eslint: 8.23.0 eslint: 8.23.0
eslint-plugin-react: 7.31.6_eslint@8.23.0 eslint-plugin-react: 7.31.6_eslint@8.23.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.23.0
mocha: 10.0.0 mocha: 10.0.0
webpack: 5.74.0_webpack-cli@4.10.0 webpack: 5.74.0_webpack-cli@4.10.0
webpack-cli: 4.10.0_webpack@5.74.0 webpack-cli: 4.10.0_webpack@5.74.0
@ -2343,6 +2345,15 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/eslint-plugin-react-hooks/4.6.0_eslint@8.23.0:
resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==}
engines: {node: '>=10'}
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
dependencies:
eslint: 8.23.0
dev: true
/eslint-plugin-react/7.31.6_eslint@8.23.0: /eslint-plugin-react/7.31.6_eslint@8.23.0:
resolution: {integrity: sha512-CXu4eu28sb8Sd2+cyUYsJVyDvpTlaXPG+bOzzpS9IzZKtye96AYX3ZmHQ6ayn/OAIQ/ufDJP8ElPWd63Pepn9w==} resolution: {integrity: sha512-CXu4eu28sb8Sd2+cyUYsJVyDvpTlaXPG+bOzzpS9IzZKtye96AYX3ZmHQ6ayn/OAIQ/ufDJP8ElPWd63Pepn9w==}
engines: {node: '>=4'} engines: {node: '>=4'}

View File

@ -1,7 +1,11 @@
{ {
"extends": ["plugin:react/recommended"], "extends": [
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"plugins": [ "plugins": [
"react" "react",
"react-hooks"
], ],
"settings": { "settings": {
"react": { "react": {

View File

@ -60,7 +60,7 @@ export default function AddonErrorModal({pluginErrors, themeErrors}) {
}, [pluginErrors, themeErrors]); }, [pluginErrors, themeErrors]);
const [tabId, setTab] = useState(tabs[0].id); const [tabId, setTab] = useState(tabs[0].id);
const switchToTab = useCallback((id) => setTab(id), [tabId]); const switchToTab = useCallback((id) => setTab(id), []);
const selectedTab = tabs.find(e => e.id === tabId); const selectedTab = tabs.find(e => e.id === tabId);
return <> return <>

View File

@ -3,15 +3,15 @@ import {React} from "modules";
const {useState, useCallback} = React; const {useState, useCallback} = React;
export default function Checkbox(props) { export default function Checkbox({checked: initialState, text, onChange: notifyParent}) {
const [checked, setChecked] = useState(props.checked); const [checked, setChecked] = useState(initialState);
const onClick = useCallback(() => { const onClick = useCallback(() => {
props?.onChange(!checked); notifyParent?.(!checked);
setChecked(!checked); setChecked(!checked);
}, [checked]); }, [notifyParent, checked]);
return <div className="checkbox-item"> return <div className="checkbox-item">
<div className="checkbox-label label-JWQiNe da-label">{props.text}</div> <div className="checkbox-label label-JWQiNe da-label">{text}</div>
<div className="checkbox-wrapper checkbox-3kaeSU da-checkbox checkbox-3EVISJ da-checkbox" onClick={onClick}> <div className="checkbox-wrapper checkbox-3kaeSU da-checkbox checkbox-3EVISJ da-checkbox" onClick={onClick}>
<div className="checkbox-inner checkboxInner-3yjcPe da-checkboxInner"> <div className="checkbox-inner checkboxInner-3yjcPe da-checkboxInner">
<input className="checkbox checkboxElement-1qV33p da-checkboxElement" checked={checked} type="checkbox" /> <input className="checkbox checkboxElement-1qV33p da-checkboxElement" checked={checked} type="checkbox" />

View File

@ -25,27 +25,27 @@ export default forwardRef(function CssEditor({css, openNative, update, save, onC
set value(newValue) {editorRef.current.setValue(newValue);}, set value(newValue) {editorRef.current.setValue(newValue);},
get hasUnsavedChanges() {return hasUnsavedChanges;} get hasUnsavedChanges() {return hasUnsavedChanges;}
}; };
}, []); }, [hasUnsavedChanges]);
useEffect(() => { useEffect(() => {
Events.on("customcss-updated", updateEditor); Events.on("customcss-updated", updateEditor);
return () => Events.off("customcss-updated", updateEditor); return () => Events.off("customcss-updated", updateEditor);
}); }, [updateEditor]);
const toggleLiveUpdate = useCallback((checked) => Settings.set("settings", "customcss", "liveUpdate", checked), []); const toggleLiveUpdate = useCallback((checked) => Settings.set("settings", "customcss", "liveUpdate", checked), []);
const updateCss = useCallback((event, newCSS) => update?.(newCSS), []); const updateCss = useCallback((event, newCSS) => update?.(newCSS), [update]);
const popoutNative = useCallback(() => openNative?.(), []); const popoutNative = useCallback(() => openNative?.(), [openNative]);
const popout = useCallback((event, currentCSS) => openDetached?.(currentCSS), []); const popout = useCallback((event, currentCSS) => openDetached?.(currentCSS), [openDetached]);
const onChange = useCallback((newCSS) => { const onChange = useCallback((newCSS) => {
notifyParent?.(newCSS); notifyParent?.(newCSS);
setUnsaved(true); setUnsaved(true);
}, []); }, [notifyParent]);
const saveCss = useCallback((event, newCSS) => { const saveCss = useCallback((event, newCSS) => {
save?.(newCSS); save?.(newCSS);
setUnsaved(false); setUnsaved(false);
}, []); }, [save]);
return <Editor return <Editor

View File

@ -34,7 +34,7 @@ export default forwardRef(function CodeEditor({value, language: requestedLang =
const [theme, setTheme] = useState(() => ThemeStore?.theme === "light" ? "vs" : "vs-dark"); const [theme, setTheme] = useState(() => ThemeStore?.theme === "light" ? "vs" : "vs-dark");
const [editor, setEditor] = useState(null); const [editor, setEditor] = useState(null);
const [bindings, setBindings] = useState([]); const [, setBindings] = useState([]);
const onThemeChange = useCallback(() => { const onThemeChange = useCallback(() => {
const newTheme = ThemeStore?.theme === "light" ? "vs" : "vs-dark"; const newTheme = ThemeStore?.theme === "light" ? "vs" : "vs-dark";
@ -45,7 +45,7 @@ export default forwardRef(function CodeEditor({value, language: requestedLang =
const onChange = useCallback(() => { const onChange = useCallback(() => {
notifyParent?.(editor?.getValue()); notifyParent?.(editor?.getValue());
}, [editor]); }, [editor, notifyParent]);
const resize = useCallback(() => editor.layout(), [editor]); const resize = useCallback(() => editor.layout(), [editor]);
const showSettings = useCallback(() => editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(editor), [editor]); const showSettings = useCallback(() => editor.keyBinding.$defaultHandler.commands.showSettingsMenu.exec(editor), [editor]);
@ -56,17 +56,20 @@ export default forwardRef(function CodeEditor({value, language: requestedLang =
get value() {return editor.getValue();}, get value() {return editor.getValue();},
set value(newValue) {editor.setValue(newValue);} set value(newValue) {editor.setValue(newValue);}
}; };
}, [editor]); }, [editor, resize, showSettings]);
useEffect(() => { useEffect(() => {
setBindings([...bindings, editor?.onDidChangeModelContent(onChange)]); setBindings(bins => [...bins, editor?.onDidChangeModelContent(onChange)]);
return () => { return () => {
for (const binding of bindings) binding?.dispose(); setBindings(bins => {
setBindings([]); for (const binding of bins) binding?.dispose();
return [];
});
}; };
}, [editor]); }, [editor, onChange]);
useEffect(() => { useEffect(() => {
let toDispose = null;
if (window.monaco?.editor) { if (window.monaco?.editor) {
const monacoEditor = window.monaco.editor.create(document.getElementById(id), { const monacoEditor = window.monaco.editor.create(document.getElementById(id), {
value: value, value: value,
@ -84,6 +87,7 @@ export default forwardRef(function CodeEditor({value, language: requestedLang =
renderWhitespace: Settings.get("settings", "editor", "renderWhitespace") renderWhitespace: Settings.get("settings", "editor", "renderWhitespace")
}); });
toDispose = monacoEditor;
setEditor(monacoEditor); setEditor(monacoEditor);
} }
else { else {
@ -91,28 +95,35 @@ export default forwardRef(function CodeEditor({value, language: requestedLang =
const textarea = document.createElement("textarea"); const textarea = document.createElement("textarea");
textarea.className = "bd-fallback-editor"; textarea.className = "bd-fallback-editor";
textarea.value = value; textarea.value = value;
textarea.onchange = (e) => onChange(e.target.value);
textarea.oninput = (e) => onChange(e.target.value);
setEditor({ setEditor({
dispose: () => textarea.remove(), dispose: () => textarea.remove(),
getValue: () => textarea.value, getValue: () => textarea.value,
setValue: (val) => textarea.value = val, setValue: (val) => textarea.value = val,
layout: () => {}, layout: () => {},
onDidChangeModelContent: (cb) => {
textarea.onchange = cb;
textarea.oninput = cb;
}
}); });
document.getElementById(id).appendChild(textarea); document.getElementById(id).appendChild(textarea);
} }
return () => {
toDispose?.dispose?.();
};
}, [id, language, theme, value]);
useEffect(() => {
ThemeStore?.addChangeListener?.(onThemeChange); ThemeStore?.addChangeListener?.(onThemeChange);
window.addEventListener("resize", resize); window.addEventListener("resize", resize);
return () => { return () => {
window.removeEventListener("resize", resize); window.removeEventListener("resize", resize);
ThemeStore?.removeChangeListener?.(onThemeChange); ThemeStore?.removeChangeListener?.(onThemeChange);
editor?.dispose();
}; };
}, []); }, [onThemeChange, resize]);
if (editor && editor.layout) editor.layout(); if (editor && editor.layout) editor.layout();

View File

@ -12,15 +12,10 @@ function minY() {
} }
export default function FloatingWindowContainer() { export default function FloatingWindowContainer() {
useEffect(() => {
Events.on("open-window", open);
return () => Events.off("open-window", open);
}, []);
const [windows, setWindows] = useState([]); const [windows, setWindows] = useState([]);
const open = useCallback(window => { const open = useCallback(window => {
setWindows([...windows, window]); setWindows(wins => [...wins, window]);
}, [windows]); }, []);
const close = useCallback(id => { const close = useCallback(id => {
setWindows(windows.filter(w => { setWindows(windows.filter(w => {
if (w.id === id && w.onClose) w.onClose(); if (w.id === id && w.onClose) w.onClose();
@ -28,6 +23,11 @@ export default function FloatingWindowContainer() {
})); }));
}, [windows]); }, [windows]);
useEffect(() => {
Events.on("open-window", open);
return () => Events.off("open-window", open);
}, [open]);
return windows.map(window => return windows.map(window =>
<FloatingWindow {...window} close={() => close(window.id)} minY={minY()} key={window.id}> <FloatingWindow {...window} close={() => close(window.id)} minY={minY()} key={window.id}>
{window.children} {window.children}

View File

@ -5,7 +5,7 @@ import CloseButton from "../icons/close";
import MaximizeIcon from "../icons/fullscreen"; import MaximizeIcon from "../icons/fullscreen";
import Modals from "../modals"; import Modals from "../modals";
const {useState, useCallback, useEffect, useRef, useMemo} = React; const {useState, useCallback, useEffect, useRef} = React;
function confirmClose(confirmationText) { function confirmClose(confirmationText) {
@ -19,18 +19,13 @@ function confirmClose(confirmationText) {
}); });
} }
export default function FloatingWindow(props) { export default function FloatingWindow({id, title, resizable, children, className, center, top: initialTop, left: initialLeft, width: initialWidth, height: initialHeight, minX = 0, minY = 0, maxX = Screen.width, maxY = Screen.height, onResize, close: doClose, confirmClose: doConfirmClose, confirmationText}) {
const [modalOpen, setOpen] = useState(false); const [modalOpen, setOpen] = useState(false);
const [isDragging, setDragging] = useState(false); const [isDragging, setDragging] = useState(false);
const [position, setPosition] = useState({x: props.center ? (Screen.width / 2) - (props.width / 2) : props.left, y: props.center ? (Screen.height / 2) - (props.height / 2) : props.top}); const [position, setPosition] = useState({x: center ? (Screen.width / 2) - (initialWidth / 2) : initialLeft, y: center ? (Screen.height / 2) - (initialHeight / 2) : initialTop});
const [offset, setOffset] = useState({x: 0, y: 0}); const [offset, setOffset] = useState({x: 0, y: 0});
const [size, setSize] = useState({width: 0, height: 0}); const [size, setSize] = useState({width: 0, height: 0});
const minX = useMemo(() => props.minX || 0);
const maxX = useMemo(() => props.maxX || Screen.width);
const minY = useMemo(() => props.minY || 0);
const maxY = useMemo(() => props.maxY || Screen.height);
const titlebar = useRef(null); const titlebar = useRef(null);
const window = useRef(null); const window = useRef(null);
@ -51,7 +46,7 @@ export default function FloatingWindow(props) {
if (newLeft + size.width >= maxX) newLeft = maxX - size.width; if (newLeft + size.width >= maxX) newLeft = maxX - size.width;
setPosition({x: newLeft, y: newTop}); setPosition({x: newLeft, y: newTop});
}, [window, offset, size, isDragging]); }, [offset, size, isDragging, minX, minY, maxX, maxY]);
const onDragStart = useCallback((e) => { const onDragStart = useCallback((e) => {
@ -66,7 +61,7 @@ export default function FloatingWindow(props) {
const width = window.current.offsetWidth; const width = window.current.offsetWidth;
const height = window.current.offsetHeight; const height = window.current.offsetHeight;
if (width != size.width || height != size.height) { if (width != size.width || height != size.height) {
if (props.onResize) props.onResize(); if (onResize) onResize();
const left = parseInt(window.current.style.left); const left = parseInt(window.current.style.left);
const top = parseInt(window.current.style.top); const top = parseInt(window.current.style.top);
if (left + width >= maxX) window.current.style.width = (maxX - left) + "px"; if (left + width >= maxX) window.current.style.width = (maxX - left) + "px";
@ -74,20 +69,22 @@ export default function FloatingWindow(props) {
} }
setSize({width, height}); setSize({width, height});
}, [window, size, onDrag]); }, [window, size, maxX, maxY, onResize]);
useEffect(() => { useEffect(() => {
window.current.addEventListener("mousedown", onResizeStart, false); const winRef = window.current;
titlebar.current.addEventListener("mousedown", onDragStart, false); const titleRef = titlebar.current;
winRef.addEventListener("mousedown", onResizeStart, false);
titleRef.addEventListener("mousedown", onDragStart, false);
document.addEventListener("mouseup", onDragStop, false); document.addEventListener("mouseup", onDragStop, false);
document.addEventListener("mousemove", onDrag, true); document.addEventListener("mousemove", onDrag, true);
return () => { return () => {
document.removeEventListener("mouseup", onDragStop, false); document.removeEventListener("mouseup", onDragStop, false);
document.removeEventListener("mousemove", onDrag, true); document.removeEventListener("mousemove", onDrag, true);
window?.current?.removeEventListener("mousedown", onResizeStart, false); winRef.removeEventListener("mousedown", onResizeStart, false);
titlebar?.current?.removeEventListener("mousedown", onDragStart, false); titleRef.removeEventListener("mousedown", onDragStart, false);
}; };
}, [titlebar, window, onDragStart, onDragStop, onDrag, onResizeStart]); }, [titlebar, window, onDragStart, onDragStop, onDrag, onResizeStart]);
@ -95,7 +92,7 @@ export default function FloatingWindow(props) {
const maximize = useCallback(() => { const maximize = useCallback(() => {
window.current.style.width = "100%"; window.current.style.width = "100%";
window.current.style.height = "100%"; window.current.style.height = "100%";
if (props.onResize) props.onResize(); if (onResize) onResize();
const width = window.current.offsetWidth; const width = window.current.offsetWidth;
const height = window.current.offsetHeight; const height = window.current.offsetHeight;
@ -123,27 +120,27 @@ export default function FloatingWindow(props) {
window.current.style.left = minX + "px"; window.current.style.left = minX + "px";
window.current.style.height = (width - difference) + "px"; window.current.style.height = (width - difference) + "px";
} }
}, [window, minX, minY, maxX, maxY]); }, [window, minX, minY, maxX, maxY, onResize]);
const close = useCallback(async () => { const close = useCallback(async () => {
let shouldClose = true; let shouldClose = true;
const didConfirmClose = typeof(props.confirmClose) == "function" ? props.confirmClose() : props.confirmClose; const didConfirmClose = typeof(doConfirmClose) == "function" ? doConfirmClose() : doConfirmClose;
if (didConfirmClose) { if (didConfirmClose) {
setOpen(true); setOpen(true);
shouldClose = await confirmClose(props.confirmationText); shouldClose = await confirmClose(confirmationText);
setOpen(false); setOpen(false);
} }
if (props.close && shouldClose) props.close(); if (doClose && shouldClose) doClose();
}, []); }, [confirmationText, doClose, doConfirmClose]);
const className = `floating-window${` ${props.className}` || ""}${props.resizable ? " resizable" : ""}${modalOpen ? " modal-open" : ""}`; const finalClassname = `floating-window${` ${className}` || ""}${resizable ? " resizable" : ""}${modalOpen ? " modal-open" : ""}`;
const styles = {height: props.height, width: props.width, left: position.x || 0, top: position.y || 0}; const styles = {height: initialHeight, width: initialWidth, left: position.x || 0, top: position.y || 0};
return <div id={props.id} className={className} ref={window} style={styles}> return <div id={id} className={finalClassname} ref={window} style={styles}>
<div className="floating-window-titlebar" ref={titlebar}> <div className="floating-window-titlebar" ref={titlebar}>
<span className="title">{props.title}</span> <span className="title">{title}</span>
<div className="floating-window-buttons"> <div className="floating-window-buttons">
<div className="button maximize-button" onClick={maximize}> <div className="button maximize-button" onClick={maximize}>
<MaximizeIcon size="18px" /> <MaximizeIcon size="18px" />
@ -154,7 +151,7 @@ export default function FloatingWindow(props) {
</div> </div>
</div> </div>
<div className="floating-window-content"> <div className="floating-window-content">
{props.children} {children}
</div> </div>
</div>; </div>;
} }

View File

@ -19,14 +19,14 @@ export default forwardRef(function AddonEditor({content, language, save, openNat
set value(newValue) {editorRef.current.setValue(newValue);}, set value(newValue) {editorRef.current.setValue(newValue);},
get hasUnsavedChanges() {return hasUnsavedChanges;} get hasUnsavedChanges() {return hasUnsavedChanges;}
}; };
}, []); }, [hasUnsavedChanges]);
const popoutNative = useCallback(() => openNative?.(), []); const popoutNative = useCallback(() => openNative?.(), [openNative]);
const onChange = useCallback(() => setUnsaved(true), []); const onChange = useCallback(() => setUnsaved(true), []);
const saveAddon = useCallback((event, newCSS) => { const saveAddon = useCallback((event, newCSS) => {
save?.(newCSS); save?.(newCSS);
setUnsaved(false); setUnsaved(false);
}, []); }, [save]);
return <Editor return <Editor
ref={editorRef} ref={editorRef}

View File

@ -27,9 +27,9 @@ export default function ServerCard({server, joined, join, navigateTo, defaultAva
setJoined("joining"); setJoined("joining");
const didJoin = await join(server.identifier, server.nativeJoin); const didJoin = await join(server.identifier, server.nativeJoin);
setJoined(didJoin); setJoined(didJoin);
}, [hasJoined]); }, [hasJoined, join, navigateTo, server.identifier, server.nativeJoin]);
const defaultIcon = useMemo(() => defaultAvatar(), []); const defaultIcon = useMemo(() => defaultAvatar(), [defaultAvatar]);
const currentIcon = !server.iconUrl || isError ? defaultIcon : server.iconUrl; const currentIcon = !server.iconUrl || isError ? defaultIcon : server.iconUrl;
const addedDate = new Date(server.insertDate * 1000); // Convert from unix timestamp const addedDate = new Date(server.insertDate * 1000); // Convert from unix timestamp

View File

@ -85,7 +85,7 @@ export default function AddonCard({addon, type, disabled, enabled, onChange: par
const onChange = useCallback(() => { const onChange = useCallback(() => {
setEnabled(!isEnabled); setEnabled(!isEnabled);
if (parentChange) parentChange(addon.id); if (parentChange) parentChange(addon.id);
}, []); }, [addon.id, parentChange, isEnabled]);
const showSettings = useCallback(() => { const showSettings = useCallback(() => {
if (!hasSettings || !enabled) return; if (!hasSettings || !enabled) return;
@ -97,7 +97,7 @@ export default function AddonCard({addon, type, disabled, enabled, onChange: par
Toasts.show(Strings.Addons.settingsError.format({name}), {type: "error"}); Toasts.show(Strings.Addons.settingsError.format({name}), {type: "error"});
Logger.stacktrace("Addon Settings", "Unable to get settings panel for " + name + ".", err); Logger.stacktrace("Addon Settings", "Unable to get settings panel for " + name + ".", err);
} }
}, [hasSettings, enabled]); }, [hasSettings, enabled, addon.name, getSettingsPanel]);
const messageAuthor = useCallback(() => { const messageAuthor = useCallback(() => {
if (!addon.authorId) return; if (!addon.authorId) return;
@ -127,7 +127,7 @@ export default function AddonCard({addon, type, disabled, enabled, onChange: par
{authorArray} {authorArray}
</div> </div>
]; ];
}, []); }, [addon.name, addon.version, addon.authorLink, addon.authorId, addon.author, messageAuthor]);
const footer = useMemo(() => { const footer = useMemo(() => {
const links = Object.keys(LinkIcons); const links = Object.keys(LinkIcons);
@ -140,7 +140,7 @@ export default function AddonCard({addon, type, disabled, enabled, onChange: par
{deleteAddon && makeButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, deleteAddon, {isControl: true, danger: true})} {deleteAddon && makeButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, deleteAddon, {isControl: true, danger: true})}
</div> </div>
</div>; </div>;
}, [hasSettings, editAddon, deleteAddon]); }, [hasSettings, editAddon, deleteAddon, addon, enabled, showSettings]);
return <div id={`${addon.id}-card`} className={"bd-addon-card" + (disabled ? " bd-addon-card-disabled" : "")}> return <div id={`${addon.id}-card`} className={"bd-addon-card" + (disabled ? " bd-addon-card-disabled" : "")}>
<div className="bd-addon-header"> <div className="bd-addon-header">

View File

@ -90,36 +90,36 @@ export default function AddonList({prefix, type, title, folder, addonList, addon
Events.off(`${prefix}-loaded`, forceUpdate); Events.off(`${prefix}-loaded`, forceUpdate);
Events.off(`${prefix}-unloaded`, forceUpdate); Events.off(`${prefix}-unloaded`, forceUpdate);
}; };
}, []); }, [prefix]);
const changeView = useCallback((value) => { const changeView = useCallback((value) => {
saveState(type, "view", value); saveState(type, "view", value);
setView(value); setView(value);
}, []); }, [type]);
const listView = useCallback(() => changeView("list"), []); const listView = useCallback(() => changeView("list"), [changeView]);
const gridView = useCallback(() => changeView("grid"), []); const gridView = useCallback(() => changeView("grid"), [changeView]);
const changeDirection = useCallback((value) => { const changeDirection = useCallback((value) => {
saveState(type, "ascending", value); saveState(type, "ascending", value);
setAscending(value); setAscending(value);
}, []); }, [type]);
const changeSort = useCallback((value) => { const changeSort = useCallback((value) => {
saveState(type, "sort", value); saveState(type, "sort", value);
setSort(value); setSort(value);
}, []); }, [type]);
const search = useCallback((e) => setQuery(e.target.value.toLocaleLowerCase()), []); const search = useCallback((e) => setQuery(e.target.value.toLocaleLowerCase()), []);
const triggerEdit = useCallback((id) => editAddon?.(id), []); const triggerEdit = useCallback((id) => editAddon?.(id), [editAddon]);
const triggerDelete = useCallback(async (id) => { const triggerDelete = useCallback(async (id) => {
const addon = addonList.find(a => a.id == id); const addon = addonList.find(a => a.id == id);
const shouldDelete = await confirmDelete(addon); const shouldDelete = await confirmDelete(addon);
if (!shouldDelete) return; if (!shouldDelete) return;
if (deleteAddon) deleteAddon(addon); if (deleteAddon) deleteAddon(addon);
}, []); }, [addonList, deleteAddon]);
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: openFolder} : null; const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: openFolder.bind(null, folder)} : null;
const renderedCards = useMemo(() => { const renderedCards = useMemo(() => {
let sorted = addonList.sort((a, b) => { let sorted = addonList.sort((a, b) => {
const sortByEnabled = sort === "isEnabled"; const sortByEnabled = sort === "isEnabled";
@ -150,7 +150,7 @@ export default function AddonList({prefix, type, title, folder, addonList, addon
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance); const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>; return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
}); });
}, [addonList, addonState, sort, ascending, query]); }, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, sort, ascending, query]);
const hasAddonsInstalled = addonList.length !== 0; const hasAddonsInstalled = addonList.length !== 0;
const isSearching = !!query; const isSearching = !!query;

View File

@ -60,7 +60,7 @@ export default function Color({value: initialValue, onChange, colors = defaultCo
const change = useCallback((e) => { const change = useCallback((e) => {
onChange?.(resolveColor(e.target.value)); onChange?.(resolveColor(e.target.value));
setValue(e.target.value); setValue(e.target.value);
}, []); }, [onChange]);
const intValue = resolveColor(value, false); const intValue = resolveColor(value, false);
return <div className="bd-color-picker-container"> return <div className="bd-color-picker-container">

View File

@ -9,7 +9,7 @@ export default function Select({value: initialValue, options, style, onChange})
const change = useCallback((val) => { const change = useCallback((val) => {
onChange?.(val); onChange?.(val);
setValue(val); setValue(val);
}, []); }, [onChange]);
const hideMenu = useCallback(() => { const hideMenu = useCallback(() => {
@ -26,7 +26,7 @@ export default function Select({value: initialValue, options, style, onChange})
setOpen(next); setOpen(next);
if (!next) return; if (!next) return;
document.addEventListener("click", hideMenu); document.addEventListener("click", hideMenu);
}, [open]); }, [hideMenu, open]);
// ?? options[0] provides a double failsafe // ?? options[0] provides a double failsafe

View File

@ -26,19 +26,19 @@ export default function Keybind({value: initialValue, onChange, max = 2, clearab
if (onChange) onChange(state.accum); if (onChange) onChange(state.accum);
setState({value: state.accum.slice(0), isRecording: false, accum: []}); setState({value: state.accum.slice(0), isRecording: false, accum: []});
} }
}, [state]); }, [state, max, onChange]);
const onClick = useCallback((e) => {
if (e.target?.className?.includes?.("bd-keybind-clear") || e.target?.closest(".bd-button")?.className?.includes("bd-keybind-clear")) return clearKeybind(e);
setState({...state, isRecording: !state.isRecording});
}, [state]);
const clearKeybind = useCallback((event) => { const clearKeybind = useCallback((event) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
if (onChange) onChange([]); if (onChange) onChange([]);
setState({...state, value: [], accum: []}); setState({...state, value: [], accum: []});
}, []); }, [onChange, state]);
const onClick = useCallback((e) => {
if (e.target?.className?.includes?.("bd-keybind-clear") || e.target?.closest(".bd-button")?.className?.includes("bd-keybind-clear")) return clearKeybind(e);
setState({...state, isRecording: !state.isRecording});
}, [state, clearKeybind]);
const displayValue = state.isRecording ? "Recording..." : !state.value.length ? "N/A" : state.value.join(" + "); const displayValue = state.isRecording ? "Recording..." : !state.value.length ? "N/A" : state.value.join(" + ");

View File

@ -8,7 +8,7 @@ export default function Number({value: initialValue, min, max, step, onChange})
const change = useCallback((e) => { const change = useCallback((e) => {
onChange?.(e.target.value); onChange?.(e.target.value);
setValue(e.target.value); setValue(e.target.value);
}, []); }, [onChange]);
return <input onChange={change} type="number" className="bd-number-input" min={min} max={max} step={step} value={value} />; return <input onChange={change} type="number" className="bd-number-input" min={min} max={max} step={step} value={value} />;
} }

View File

@ -12,7 +12,7 @@ export default function Radio({name, value, options, onChange}) {
const newValue = options[newIndex].value; const newValue = options[newIndex].value;
onChange?.(newValue); onChange?.(newValue);
setIndex(newIndex); setIndex(newIndex);
}, [index, options]); }, [options, onChange]);
function renderOption(opt, i) { function renderOption(opt, i) {
const isSelected = index === i; const isSelected = index === i;

View File

@ -9,7 +9,7 @@ export default function Search({onChange, className, onKeyDown, placeholder}) {
const change = useCallback((e) => { const change = useCallback((e) => {
onChange?.(e); onChange?.(e);
setValue(e.target.value); setValue(e.target.value);
}, []); }, [onChange]);
return <div className={"bd-search-wrapper" + (className ? ` ${className}` : "")}> return <div className={"bd-search-wrapper" + (className ? ` ${className}` : "")}>

View File

@ -8,7 +8,7 @@ export default function Slider({value: initialValue, min, max, step, onChange})
const change = useCallback((e) => { const change = useCallback((e) => {
onChange?.(e.target.value); onChange?.(e.target.value);
setValue(e.target.value); setValue(e.target.value);
}, []); }, [onChange]);
return <div className="bd-slider-wrap"> return <div className="bd-slider-wrap">
<div className="bd-slider-label">{value}</div><input onChange={change} type="range" className="bd-slider-input" min={min} max={max} step={step} value={value} style={{backgroundSize: (value - min) * 100 / (max - min) + "% 100%"}} /> <div className="bd-slider-label">{value}</div><input onChange={change} type="range" className="bd-slider-input" min={min} max={max} step={step} value={value} style={{backgroundSize: (value - min) * 100 / (max - min) + "% 100%"}} />

View File

@ -8,7 +8,7 @@ export default function Switch({id, checked: initialValue, disabled, onChange})
const change = useCallback(() => { const change = useCallback(() => {
onChange?.(!checked); onChange?.(!checked);
setChecked(!checked); setChecked(!checked);
}, [checked]); }, [checked, onChange]);
const enabledClass = disabled ? " bd-switch-disabled" : ""; const enabledClass = disabled ? " bd-switch-disabled" : "";
const checkedClass = checked ? " bd-switch-checked" : ""; const checkedClass = checked ? " bd-switch-checked" : "";

View File

@ -8,7 +8,7 @@ export default function Textbox({value: initialValue, maxLength, placeholder, on
const change = useCallback((e) => { const change = useCallback((e) => {
onChange?.(e.target.value); onChange?.(e.target.value);
setValue(e.target.value); setValue(e.target.value);
}, []); }, [onChange]);
return <input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-text-input" placeholder={placeholder} maxLength={maxLength} value={value} />; return <input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-text-input" placeholder={placeholder} maxLength={maxLength} value={value} />;
} }

View File

@ -22,7 +22,7 @@ export default function Drawer({name, collapsible, shown = true, showDivider, ch
drawer.classList.remove("animating"); drawer.classList.remove("animating");
}, timeout); }, timeout);
}, [collapsed]); }, [collapsed, onDrawerToggle]);
const onClick = useCallback((event) => { const onClick = useCallback((event) => {

View File

@ -17,7 +17,7 @@ export default function Group({onChange, id, name, button, shown, onDrawerToggle
const change = useCallback((settingId, value) => { const change = useCallback((settingId, value) => {
if (id) onChange?.(id, settingId, value); if (id) onChange?.(id, settingId, value);
else onChange?.(settingId, value); else onChange?.(settingId, value);
}, [id]); }, [id, onChange]);
return <Drawer collapsible={collapsible} name={name} button={button} shown={shown} onDrawerToggle={onDrawerToggle} showDivider={showDivider}> return <Drawer collapsible={collapsible} name={name} button={button} shown={shown} onDrawerToggle={onDrawerToggle} showDivider={showDivider}>
{settings.filter(s => !s.hidden).map((setting) => { {settings.filter(s => !s.hidden).map((setting) => {

View File

@ -11,7 +11,7 @@ export default function SettingsTitle({isGroup, className, button, onClick, text
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
button?.onClick?.(event); button?.onClick?.(event);
}, []); }, [button]);
const baseClass = isGroup ? groupClass : basicClass; const baseClass = isGroup ? groupClass : basicClass;

View File

@ -9,50 +9,50 @@ import Checkmark from "./icons/check";
const {useState, useCallback, useEffect} = React; const {useState, useCallback, useEffect} = React;
function CoreUpdaterPanel(props) { function CoreUpdaterPanel({hasUpdate, remoteVersion, update}) {
return <Drawer name="BetterDiscord" collapsible={true}> return <Drawer name="BetterDiscord" collapsible={true}>
<SettingItem name={`Core v${Config.version}`} note={props.hasUpdate ? Strings.Updater.versionAvailable.format({version: props.remoteVersion}) : Strings.Updater.noUpdatesAvailable} inline={true} id={"core-updater"}> <SettingItem name={`Core v${Config.version}`} note={hasUpdate ? Strings.Updater.versionAvailable.format({version: remoteVersion}) : Strings.Updater.noUpdatesAvailable} inline={true} id={"core-updater"}>
{!props.hasUpdate && <div className="bd-filled-checkmark"><Checkmark /></div>} {!hasUpdate && <div className="bd-filled-checkmark"><Checkmark /></div>}
{props.hasUpdate && <button className="bd-button" onClick={props.update}>{Strings.Updater.updateButton}</button>} {hasUpdate && <button className="bd-button" onClick={update}>{Strings.Updater.updateButton}</button>}
</SettingItem> </SettingItem>
</Drawer>; </Drawer>;
} }
function NoUpdates(props) { function NoUpdates({type}) {
return <div className="bd-empty-updates"> return <div className="bd-empty-updates">
<Checkmark size="48px" /> <Checkmark size="48px" />
{Strings.Updater.upToDateBlankslate.format({type: props.type})} {Strings.Updater.upToDateBlankslate.format({type: type})}
</div>; </div>;
} }
function AddonUpdaterPanel(props) { function AddonUpdaterPanel({pending, type, updater, update, updateAll}) {
const filenames = props.pending; const filenames = pending;
return <Drawer name={Strings.Panels[props.type]} collapsible={true} button={filenames.length ? {title: Strings.Updater.updateAll, onClick: () => props.updateAll(props.type)} : null}> return <Drawer name={Strings.Panels[type]} collapsible={true} button={filenames.length ? {title: Strings.Updater.updateAll, onClick: () => updateAll(type)} : null}>
{!filenames.length && <NoUpdates type={props.type} />} {!filenames.length && <NoUpdates type={type} />}
{filenames.map(f => { {filenames.map(f => {
const info = props.updater.cache[f]; const info = updater.cache[f];
const addon = props.updater.manager.addonList.find(a => a.filename === f); const addon = updater.manager.addonList.find(a => a.filename === f);
return <SettingItem name={`${addon.name} v${addon.version}`} note={Strings.Updater.versionAvailable.format({version: info.version})} inline={true} id={addon.name}> return <SettingItem name={`${addon.name} v${addon.version}`} note={Strings.Updater.versionAvailable.format({version: info.version})} inline={true} id={addon.name}>
<button className="bd-button" onClick={() => props.update(props.type, f)}>{Strings.Updater.updateButton}</button> <button className="bd-button" onClick={() => update(type, f)}>{Strings.Updater.updateButton}</button>
</SettingItem>; </SettingItem>;
})} })}
</Drawer>; </Drawer>;
} }
export default function UpdaterPanel(props) { export default function UpdaterPanel({coreUpdater, pluginUpdater, themeUpdater}) {
const [hasCoreUpdate, setCoreUpdate] = useState(props.coreUpdater.hasUpdate); const [hasCoreUpdate, setCoreUpdate] = useState(coreUpdater.hasUpdate);
const [updates, setUpdates] = useState({plugins: props.pluginUpdater.pending.slice(0), themes: props.themeUpdater.pending.slice(0)}); const [updates, setUpdates] = useState({plugins: pluginUpdater.pending.slice(0), themes: themeUpdater.pending.slice(0)});
const checkAddons = useCallback(async (type) => { const checkAddons = useCallback(async (type) => {
const updater = type === "plugins" ? props.pluginUpdater : props.themeUpdater; const updater = type === "plugins" ? pluginUpdater : themeUpdater;
await updater.checkAll(false); await updater.checkAll(false);
setUpdates({...updates, [type]: updater.pending.slice(0)}); setUpdates({...updates, [type]: updater.pending.slice(0)});
}, []); }, [updates, pluginUpdater, themeUpdater]);
const update = useCallback(() => { const update = useCallback(() => {
checkAddons("plugins"); checkAddons("plugins");
checkAddons("themes"); checkAddons("themes");
}, []); }, [checkAddons]);
useEffect(() => { useEffect(() => {
Events.on(`plugin-loaded`, update); Events.on(`plugin-loaded`, update);
@ -65,12 +65,12 @@ export default function UpdaterPanel(props) {
Events.off(`theme-loaded`, update); Events.off(`theme-loaded`, update);
Events.off(`theme-unloaded`, update); Events.off(`theme-unloaded`, update);
}; };
}, []); }, [update]);
const checkCoreUpdate = useCallback(async () => { const checkCoreUpdate = useCallback(async () => {
await props.coreUpdater.checkForUpdate(false); await coreUpdater.checkForUpdate(false);
setCoreUpdate(props.coreUpdater.hasUpdate); setCoreUpdate(coreUpdater.hasUpdate);
}, []); }, [coreUpdater]);
const checkForUpdates = useCallback(async () => { const checkForUpdates = useCallback(async () => {
Toasts.info(Strings.Updater.checking); Toasts.info(Strings.Updater.checking);
@ -78,33 +78,33 @@ export default function UpdaterPanel(props) {
await checkAddons("plugins"); await checkAddons("plugins");
await checkAddons("themes"); await checkAddons("themes");
Toasts.info(Strings.Updater.finishedChecking); Toasts.info(Strings.Updater.finishedChecking);
}); }, [checkAddons, checkCoreUpdate]);
const updateCore = useCallback(async () => { const updateCore = useCallback(async () => {
await props.coreUpdater.update(); await coreUpdater.update();
setCoreUpdate(false); setCoreUpdate(false);
}, []); }, [coreUpdater]);
const updateAddon = useCallback(async (type, filename) => { const updateAddon = useCallback(async (type, filename) => {
const updater = type === "plugins" ? props.pluginUpdater : props.themeUpdater; const updater = type === "plugins" ? pluginUpdater : themeUpdater;
await updater.updateAddon(filename); await updater.updateAddon(filename);
setUpdates(prev => { setUpdates(prev => {
prev[type].splice(prev[type].indexOf(filename), 1); prev[type].splice(prev[type].indexOf(filename), 1);
return prev; return prev;
}); });
}, []); }, [pluginUpdater, themeUpdater]);
const updateAllAddons = useCallback(async (type) => { const updateAllAddons = useCallback(async (type) => {
const toUpdate = updates[type].slice(0); const toUpdate = updates[type].slice(0);
for (const filename of toUpdate) { for (const filename of toUpdate) {
await updateAddon(type, filename); await updateAddon(type, filename);
} }
}, []); }, [updateAddon, updates]);
return [ return [
<SettingsTitle text={Strings.Panels.updates} button={{title: Strings.Updater.checkForUpdates, onClick: checkForUpdates}} />, <SettingsTitle text={Strings.Panels.updates} button={{title: Strings.Updater.checkForUpdates, onClick: checkForUpdates}} />,
<CoreUpdaterPanel remoteVersion={props.coreUpdater.remoteVersion} hasUpdate={hasCoreUpdate} update={updateCore} />, <CoreUpdaterPanel remoteVersion={coreUpdater.remoteVersion} hasUpdate={hasCoreUpdate} update={updateCore} />,
<AddonUpdaterPanel type="plugins" pending={updates.plugins} update={updateAddon} updateAll={updateAllAddons} updater={props.pluginUpdater} />, <AddonUpdaterPanel type="plugins" pending={updates.plugins} update={updateAddon} updateAll={updateAllAddons} updater={pluginUpdater} />,
<AddonUpdaterPanel type="themes" pending={updates.themes} update={updateAddon} updateAll={updateAllAddons} updater={props.themeUpdater} />, <AddonUpdaterPanel type="themes" pending={updates.themes} update={updateAddon} updateAll={updateAllAddons} updater={themeUpdater} />,
]; ];
} }