Convert various remaining components

This commit is contained in:
Zack Rauen 2023-03-08 20:09:10 -05:00
parent 717c9026f4
commit 001a50e762
3 changed files with 163 additions and 225 deletions

View File

@ -3,24 +3,19 @@ import Extension from "./icons/extension";
import ThemeIcon from "./icons/theme"; import ThemeIcon from "./icons/theme";
import Divider from "./divider"; import Divider from "./divider";
const Parser = Object(WebpackModules.getByProps("defaultRules", "parse")).defaultRules; const Parser = Object(WebpackModules.getByProps("defaultRules", "parse")).defaultRules;
const {useState, useCallback, useMemo} = React;
const joinClassNames = (...classNames) => classNames.filter(e => e).join(" "); const joinClassNames = (...classNames) => classNames.filter(e => e).join(" ");
class AddonError extends React.Component { function AddonError({err, index}) {
constructor(props) { const [expanded, setExpanded] = useState(false);
super(props); const toggle = useCallback(() => setExpanded(!expanded), [expanded]);
this.state = { function renderErrorBody() {
expanded: false
};
}
toggle() {
this.setState({expanded: !this.state.expanded});
}
renderErrorBody(err) {
const stack = err?.error?.stack ?? err.stack; const stack = err?.error?.stack ?? err.stack;
if (!this.state.expanded || !stack) return null; if (!expanded || !stack) return null;
return <div className="bd-addon-error-body"> return <div className="bd-addon-error-body">
<Divider /> <Divider />
<div className="bd-addon-error-stack"> <div className="bd-addon-error-stack">
@ -28,92 +23,57 @@ class AddonError extends React.Component {
</div> </div>
</div>; </div>;
} }
render() {
const err = this.props.err; return <div key={`${err.type}-${index}`} className={joinClassNames("bd-addon-error", (expanded) ? "expanded" : "collapsed")}>
return <div key={`${err.type}-${this.props.index}`} className={joinClassNames("bd-addon-error", (this.state.expanded) ? "expanded" : "collapsed")}> <div className="bd-addon-error-header" onClick={toggle} >
<div className="bd-addon-error-header" onClick={() => {this.toggle();}} > <div className="bd-addon-error-icon">
<div className="bd-addon-error-icon"> {err.type == "plugin" ? <Extension /> : <ThemeIcon />}
{err.type == "plugin" ? <Extension /> : <ThemeIcon />}
</div>
<div className="bd-addon-error-header-inner">
<h3 className={`bd-addon-error-file ${DiscordClasses.Text.colorHeaderPrimary} ${DiscordClasses.Integrations.secondaryHeader} ${DiscordClasses.Text.size16}`}>{err.name}</h3>
<div className={`bd-addon-error-details ${DiscordClasses.Integrations.detailsWrapper}`}>
<svg className={DiscordClasses.Integrations.detailsIcon} aria-hidden="false" width="16" height="16" viewBox="0 0 12 12">
<path fill="currentColor" d="M6 1C3.243 1 1 3.244 1 6c0 2.758 2.243 5 5 5s5-2.242 5-5c0-2.756-2.243-5-5-5zm0 2.376a.625.625 0 110 1.25.625.625 0 010-1.25zM7.5 8.5h-3v-1h1V6H5V5h1a.5.5 0 01.5.5v2h1v1z"></path>
</svg>
<div className={`${DiscordClasses.Text.colorHeaderSecondary} ${DiscordClasses.Text.size12}`}>{err.message}</div>
</div>
</div>
<svg className="bd-addon-error-expander" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M7 10L12 15 17 10" aria-hidden="true"></path>
</svg>
</div> </div>
{this.renderErrorBody(err)} <div className="bd-addon-error-header-inner">
</div>; <h3 className={`bd-addon-error-file ${DiscordClasses.Text.colorHeaderPrimary} ${DiscordClasses.Integrations.secondaryHeader} ${DiscordClasses.Text.size16}`}>{err.name}</h3>
} <div className={`bd-addon-error-details ${DiscordClasses.Integrations.detailsWrapper}`}>
<svg className={DiscordClasses.Integrations.detailsIcon} aria-hidden="false" width="16" height="16" viewBox="0 0 12 12">
<path fill="currentColor" d="M6 1C3.243 1 1 3.244 1 6c0 2.758 2.243 5 5 5s5-2.242 5-5c0-2.756-2.243-5-5-5zm0 2.376a.625.625 0 110 1.25.625.625 0 010-1.25zM7.5 8.5h-3v-1h1V6H5V5h1a.5.5 0 01.5.5v2h1v1z"></path>
</svg>
<div className={`${DiscordClasses.Text.colorHeaderSecondary} ${DiscordClasses.Text.size12}`}>{err.message}</div>
</div>
</div>
<svg className="bd-addon-error-expander" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M7 10L12 15 17 10" aria-hidden="true"></path>
</svg>
</div>
{renderErrorBody(err)}
</div>;
} }
export default class AddonErrorModal extends React.Component {
constructor(props) {
super(props);
const tabs = this.getTabs();
this.state = { function generateTab(id, errors) {
selectedTab: tabs[0].id, return {id, errors, name: Strings.Panels[id]};
}; }
}
mergeErrors(errors1 = [], errors2 = []) { export default function AddonErrorModal({pluginErrors, themeErrors}) {
const list = []; const tabs = useMemo(() => {
const allErrors = [...errors2, ...errors1]; return [
for (const error of allErrors) { pluginErrors.length && generateTab("plugins", pluginErrors),
if (list.find(e => e.file === error.file)) continue; themeErrors.length && generateTab("themes", themeErrors)
list.push(error); ].filter(e => e);
} }, [pluginErrors, themeErrors]);
return list;
}
refreshTabs(pluginErrors, themeErrors) { const [tabId, setTab] = useState(tabs[0].id);
this._tabs = null; const switchToTab = useCallback((id) => setTab(id), [tabId]);
this.props.pluginErrors = this.mergeErrors(this.props.pluginErrors, pluginErrors); const selectedTab = tabs.find(e => e.id === tabId);
this.props.themeErrors = this.mergeErrors(this.props.themeErrors, themeErrors);
this.forceUpdate();
}
generateTab(id, errors) { return <>
return { <div className={`bd-error-modal-header ${DiscordClasses.Modal.header} ${DiscordClasses.Modal.separator}`}>
id: id, <h4 className={`${DiscordClasses.Titles.defaultColor} ${DiscordClasses.Text.size14} ${DiscordClasses.Titles.h4} ${DiscordClasses.Margins.marginBottom8}`}>{Strings.Modals.addonErrors}</h4>
name: Strings.Panels[id], <div className="bd-tab-bar">
errors: errors {tabs.map(tab => <div onClick={() => {switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
};
}
getTabs() {
return this._tabs || (this._tabs = [
this.props.pluginErrors.length && this.generateTab("plugins", this.props.pluginErrors),
this.props.themeErrors.length && this.generateTab("themes", this.props.themeErrors)
].filter(e => e));
}
switchToTab(id) {
this.setState({selectedTab: id});
}
render() {
const selectedTab = this.getTabs().find(e => this.state.selectedTab === e.id);
const tabs = this.getTabs();
return <>
<div className={`bd-error-modal-header ${DiscordClasses.Modal.header} ${DiscordClasses.Modal.separator}`}>
<h4 className={`${DiscordClasses.Titles.defaultColor} ${DiscordClasses.Text.size14} ${DiscordClasses.Titles.h4} ${DiscordClasses.Margins.marginBottom8}`}>{Strings.Modals.addonErrors}</h4>
<div className="bd-tab-bar">
{tabs.map(tab => <div onClick={() => {this.switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
</div>
</div> </div>
<div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}> </div>
<div className="bd-addon-errors"> <div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}>
{selectedTab.errors.map((error, index) => <AddonError index={index} err={error} />)} <div className="bd-addon-errors">
</div> {selectedTab.errors.map((error, index) => <AddonError index={index} err={error} />)}
</div> </div>
</>; </div>
} </>;
} }

View File

@ -1,70 +1,62 @@
import {React, Strings} from "modules"; import {React, Strings} from "modules";
const badge = <div className="flowerStarContainer-3zDVtj verified-1eC5dy background-2uufRq guildBadge-RlDbED" const {useState, useCallback, useMemo} = React;
style={{width: "16px", height: "16px"}}>
<svg aria-label="Verified &amp; Partnered" className="flowerStar-1GeTsn"
aria-hidden="false" width="16" height="16" viewBox="0 0 16 15.2">
<path fill="currentColor" fillRule="evenodd"
d="m16 7.6c0 .79-1.28 1.38-1.52 2.09s.44 2 0 2.59-1.84.35-2.46.8-.79 1.84-1.54 2.09-1.67-.8-2.47-.8-1.75 1-2.47.8-.92-1.64-1.54-2.09-2-.18-2.46-.8.23-1.84 0-2.59-1.54-1.3-1.54-2.09 1.28-1.38 1.52-2.09-.44-2 0-2.59 1.85-.35 2.48-.8.78-1.84 1.53-2.12 1.67.83 2.47.83 1.75-1 2.47-.8.91 1.64 1.53 2.09 2 .18 2.46.8-.23 1.84 0 2.59 1.54 1.3 1.54 2.09z">
</path>
</svg>
<div className="childContainer-1wxZNh">
<svg className="icon-1ihkOt" aria-hidden="false" width="16" height="16" viewBox="0 0 16 15.2">
<path d="M7.4,11.17,4,8.62,5,7.26l2,1.53L10.64,4l1.36,1Z" fill="currentColor"></path>
</svg>
</div>
</div>;
export default class ServerCard extends React.Component {
constructor(props) {
super(props);
if (!this.props.server.iconUrl) this.props.server.iconUrl = this.props.defaultAvatar();
this.state = {
joined: this.props.joined
};
this.join = this.join.bind(this);
this.handleError = this.handleError.bind(this);
}
render() { const badge = <div className="flowerStarContainer-1QeD-L verified-1Jv_7P background-3Da2vZ rowIcon-2tDEcE" style={{width: "16px", height: "16px"}}>
const {server} = this.props; <svg aria-label="Verified &amp; Partnered" className="flowerStar-2tNFCR" aria-hidden="false" width="16" height="16" viewBox="0 0 16 15.2">
const addedDate = new Date(server.insertDate * 1000); // Convert from unix timestamp <path fill="currentColor" fillRule="evenodd" d="m16 7.6c0 .79-1.28 1.38-1.52 2.09s.44 2 0 2.59-1.84.35-2.46.8-.79 1.84-1.54 2.09-1.67-.8-2.47-.8-1.75 1-2.47.8-.92-1.64-1.54-2.09-2-.18-2.46-.8.23-1.84 0-2.59-1.54-1.3-1.54-2.09 1.28-1.38 1.52-2.09-.44-2 0-2.59 1.85-.35 2.48-.8.78-1.84 1.53-2.12 1.67.83 2.47.83 1.75-1 2.47-.8.91 1.64 1.53 2.09 2 .18 2.46.8-.23 1.84 0 2.59 1.54 1.3 1.54 2.09z"></path>
const buttonText = typeof(this.state.joined) == "string" ? `${Strings.PublicServers.joining}...` : this.state.joined ? Strings.PublicServers.joined : Strings.PublicServers.join; </svg>
<div className="childContainer-U_a6Yh">
return <div className="bd-server-card" role="button" tabIndex="0" onClick={this.join}> <svg className="icon-3BYlXK" aria-hidden="false" width="16" height="16" viewBox="0 0 16 15.2">
<div className="bd-server-header"> <path d="M7.4,11.17,4,8.62,5,7.26l2,1.53L10.64,4l1.36,1Z" fill="currentColor"></path>
<div className="bd-server-splash-container"><img src={server.iconUrl} onError={this.handleError} className="bd-server-splash" /></div> </svg>
<img src={server.iconUrl} onError={this.handleError} className="bd-server-icon" />
</div>
<div className="bd-server-info">
<div className="bd-server-title">
{server.pinned && badge}
<div className="bd-server-name">{server.name}</div>
{this.state.joined && <div className="bd-server-tag">{buttonText}</div>}
</div>
<div className="bd-server-description">{server.description}</div>
<div className="bd-server-footer">
<div className="bd-server-count">
<div className="bd-server-count-dot"></div>
<div className="bd-server-count-text">{server.members.toLocaleString()} Members</div>
</div>
<div className="bd-server-count">
<div className="bd-server-count-dot"></div>
<div className="bd-server-count-text">Added {addedDate.toLocaleDateString()}</div>
</div>
</div>
</div> </div>
</div>; </div>;
}
handleError() {
this.props.server.iconUrl = this.props.defaultAvatar();
}
async join() { export default function ServerCard({server, joined, join, navigateTo, defaultAvatar}) {
if (this.state.joined) return this.props.navigateTo(this.props.server.identifier); const [isError, setError] = useState(false);
this.setState({joined: "joining"}); const handleError = useCallback(() => {
const didJoin = await this.props.join(this.props.server.identifier, this.props.server.nativejoin); setError(true);
this.setState({joined: didJoin}); }, []);
}
const [hasJoined, setJoined] = useState(joined);
const doJoin = useCallback(async () => {
if (hasJoined) return navigateTo(server.identifier);
setJoined("joining");
const didJoin = await join(server.identifier, server.nativeJoin);
setJoined(didJoin);
}, [hasJoined]);
const defaultIcon = useMemo(() => defaultAvatar(), []);
const currentIcon = !server.iconUrl || isError ? defaultIcon : server.iconUrl;
const addedDate = new Date(server.insertDate * 1000); // Convert from unix timestamp
const buttonText = typeof(hasJoined) == "string" ? `${Strings.PublicServers.joining}...` : hasJoined ? Strings.PublicServers.joined : Strings.PublicServers.join;
return <div className="bd-server-card" role="button" tabIndex="0" onClick={doJoin}>
<div className="bd-server-header">
<div className="bd-server-splash-container"><img src={currentIcon} onError={handleError} className="bd-server-splash" /></div>
<img src={currentIcon} onError={handleError} className="bd-server-icon" />
</div>
<div className="bd-server-info">
<div className="bd-server-title">
{server.pinned && badge}
<div className="bd-server-name">{server.name}</div>
{hasJoined && <div className="bd-server-tag">{buttonText}</div>}
</div>
<div className="bd-server-description">{server.description}</div>
<div className="bd-server-footer">
<div className="bd-server-count">
<div className="bd-server-count-dot"></div>
<div className="bd-server-count-text">{server.members.toLocaleString()} Members</div>
</div>
<div className="bd-server-count">
<div className="bd-server-count-dot"></div>
<div className="bd-server-count-text">Added {addedDate.toLocaleDateString()}</div>
</div>
</div>
</div>
</div>;
} }

View File

@ -7,6 +7,8 @@ import Toasts from "./toasts";
import Checkmark from "./icons/check"; import Checkmark from "./icons/check";
const {useState, useCallback, useEffect} = React;
function CoreUpdaterPanel(props) { function CoreUpdaterPanel(props) {
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={props.hasUpdate ? Strings.Updater.versionAvailable.format({version: props.remoteVersion}) : Strings.Updater.noUpdatesAvailable} inline={true} id={"core-updater"}>
@ -37,88 +39,72 @@ function AddonUpdaterPanel(props) {
</Drawer>; </Drawer>;
} }
export default class UpdaterPanel extends React.Component { export default function UpdaterPanel(props) {
constructor(props) { const [hasCoreUpdate, setCoreUpdate] = useState(props.coreUpdater.hasUpdate);
super(props); const [updates, setUpdates] = useState({plugins: props.pluginUpdater.pending.slice(0), themes: props.themeUpdater.pending.slice(0)});
this.state = { const checkAddons = useCallback(async (type) => {
hasCoreUpdate: this.props.coreUpdater.hasUpdate, const updater = type === "plugins" ? props.pluginUpdater : props.themeUpdater;
plugins: this.props.pluginUpdater.pending.slice(0),
themes: this.props.themeUpdater.pending.slice(0)
};
this.checkForUpdates = this.checkForUpdates.bind(this);
this.updateAddon = this.updateAddon.bind(this);
this.updateCore = this.updateCore.bind(this);
this.updateAllAddons = this.updateAllAddons.bind(this);
this.update = this.update.bind(this);
}
update() {
this.checkAddons("plugins");
this.checkAddons("themes");
}
componentDidMount() {
Events.on(`plugin-loaded`, this.update);
Events.on(`plugin-unloaded`, this.update);
Events.on(`theme-loaded`, this.update);
Events.on(`theme-unloaded`, this.update);
}
componentWillUnmount() {
Events.off(`plugin-loaded`, this.update);
Events.off(`plugin-unloaded`, this.update);
Events.off(`theme-loaded`, this.update);
Events.off(`theme-unloaded`, this.update);
}
async checkForUpdates() {
Toasts.info(Strings.Updater.checking);
await this.checkCoreUpdate();
await this.checkAddons("plugins");
await this.checkAddons("themes");
Toasts.info(Strings.Updater.finishedChecking);
}
async checkCoreUpdate() {
await this.props.coreUpdater.checkForUpdate(false);
this.setState({hasCoreUpdate: this.props.coreUpdater.hasUpdate});
}
async updateCore() {
await this.props.coreUpdater.update();
this.setState({hasCoreUpdate: false});
}
async checkAddons(type) {
const updater = type === "plugins" ? this.props.pluginUpdater : this.props.themeUpdater;
await updater.checkAll(false); await updater.checkAll(false);
this.setState({[type]: updater.pending.slice(0)}); setUpdates({...updates, [type]: updater.pending.slice(0)});
} }, []);
async updateAddon(type, filename) { const update = useCallback(() => {
const updater = type === "plugins" ? this.props.pluginUpdater : this.props.themeUpdater; checkAddons("plugins");
checkAddons("themes");
}, []);
useEffect(() => {
Events.on(`plugin-loaded`, update);
Events.on(`plugin-unloaded`, update);
Events.on(`theme-loaded`, update);
Events.on(`theme-unloaded`, update);
return () => {
Events.off(`plugin-loaded`, update);
Events.off(`plugin-unloaded`, update);
Events.off(`theme-loaded`, update);
Events.off(`theme-unloaded`, update);
};
}, []);
const checkCoreUpdate = useCallback(async () => {
await props.coreUpdater.checkForUpdate(false);
setCoreUpdate(props.coreUpdater.hasUpdate);
}, []);
const checkForUpdates = useCallback(async () => {
Toasts.info(Strings.Updater.checking);
await checkCoreUpdate();
await checkAddons("plugins");
await checkAddons("themes");
Toasts.info(Strings.Updater.finishedChecking);
});
const updateCore = useCallback(async () => {
await props.coreUpdater.update();
setCoreUpdate(false);
}, []);
const updateAddon = useCallback(async (type, filename) => {
const updater = type === "plugins" ? props.pluginUpdater : props.themeUpdater;
await updater.updateAddon(filename); await updater.updateAddon(filename);
this.setState(prev => { setUpdates(prev => {
prev[type].splice(prev[type].indexOf(filename), 1); prev[type].splice(prev[type].indexOf(filename), 1);
return prev; return prev;
}); });
} }, []);
async updateAllAddons(type) { const updateAllAddons = useCallback(async (type) => {
const toUpdate = this.state[type].slice(0); const toUpdate = updates[type].slice(0);
for (const filename of toUpdate) { for (const filename of toUpdate) {
await this.updateAddon(type, filename); await updateAddon(type, filename);
} }
} }, []);
render() { 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: this.checkForUpdates}} />, <CoreUpdaterPanel remoteVersion={props.coreUpdater.remoteVersion} hasUpdate={hasCoreUpdate} update={updateCore} />,
<CoreUpdaterPanel remoteVersion={this.props.coreUpdater.remoteVersion} hasUpdate={this.state.hasCoreUpdate} update={this.updateCore} />, <AddonUpdaterPanel type="plugins" pending={updates.plugins} update={updateAddon} updateAll={updateAllAddons} updater={props.pluginUpdater} />,
<AddonUpdaterPanel type="plugins" pending={this.state.plugins} update={this.updateAddon} updateAll={this.updateAllAddons} updater={this.props.pluginUpdater} />, <AddonUpdaterPanel type="themes" pending={updates.themes} update={updateAddon} updateAll={updateAllAddons} updater={props.themeUpdater} />,
<AddonUpdaterPanel type="themes" pending={this.state.themes} update={this.updateAddon} updateAll={this.updateAllAddons} updater={this.props.themeUpdater} />, ];
];
}
} }