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 Divider from "./divider";
const Parser = Object(WebpackModules.getByProps("defaultRules", "parse")).defaultRules;
const {useState, useCallback, useMemo} = React;
const joinClassNames = (...classNames) => classNames.filter(e => e).join(" ");
class AddonError extends React.Component {
constructor(props) {
super(props);
function AddonError({err, index}) {
const [expanded, setExpanded] = useState(false);
const toggle = useCallback(() => setExpanded(!expanded), [expanded]);
this.state = {
expanded: false
};
}
toggle() {
this.setState({expanded: !this.state.expanded});
}
renderErrorBody(err) {
function renderErrorBody() {
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">
<Divider />
<div className="bd-addon-error-stack">
@ -28,92 +23,57 @@ class AddonError extends React.Component {
</div>
</div>;
}
render() {
const err = this.props.err;
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={() => {this.toggle();}} >
<div className="bd-addon-error-icon">
{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>
return <div key={`${err.type}-${index}`} className={joinClassNames("bd-addon-error", (expanded) ? "expanded" : "collapsed")}>
<div className="bd-addon-error-header" onClick={toggle} >
<div className="bd-addon-error-icon">
{err.type == "plugin" ? <Extension /> : <ThemeIcon />}
</div>
{this.renderErrorBody(err)}
</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>
{renderErrorBody(err)}
</div>;
}
export default class AddonErrorModal extends React.Component {
constructor(props) {
super(props);
const tabs = this.getTabs();
this.state = {
selectedTab: tabs[0].id,
};
}
function generateTab(id, errors) {
return {id, errors, name: Strings.Panels[id]};
}
mergeErrors(errors1 = [], errors2 = []) {
const list = [];
const allErrors = [...errors2, ...errors1];
for (const error of allErrors) {
if (list.find(e => e.file === error.file)) continue;
list.push(error);
}
return list;
}
export default function AddonErrorModal({pluginErrors, themeErrors}) {
const tabs = useMemo(() => {
return [
pluginErrors.length && generateTab("plugins", pluginErrors),
themeErrors.length && generateTab("themes", themeErrors)
].filter(e => e);
}, [pluginErrors, themeErrors]);
refreshTabs(pluginErrors, themeErrors) {
this._tabs = null;
this.props.pluginErrors = this.mergeErrors(this.props.pluginErrors, pluginErrors);
this.props.themeErrors = this.mergeErrors(this.props.themeErrors, themeErrors);
this.forceUpdate();
}
const [tabId, setTab] = useState(tabs[0].id);
const switchToTab = useCallback((id) => setTab(id), [tabId]);
const selectedTab = tabs.find(e => e.id === tabId);
generateTab(id, errors) {
return {
id: id,
name: Strings.Panels[id],
errors: errors
};
}
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>
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={() => {switchToTab(tab.id);}} className={joinClassNames("bd-tab-item", tab.id === selectedTab.id && "selected")}>{tab.name}</div>)}
</div>
<div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}>
<div className="bd-addon-errors">
{selectedTab.errors.map((error, index) => <AddonError index={index} err={error} />)}
</div>
</div>
<div className={`bd-error-modal-content ${DiscordClasses.Modal.content} ${DiscordClasses.Scrollers.thin}`}>
<div className="bd-addon-errors">
{selectedTab.errors.map((error, index) => <AddonError index={index} err={error} />)}
</div>
</>;
}
</div>
</>;
}

View File

@ -1,70 +1,62 @@
import {React, Strings} from "modules";
const badge = <div className="flowerStarContainer-3zDVtj verified-1eC5dy background-2uufRq guildBadge-RlDbED"
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>;
const {useState, useCallback, useMemo} = React;
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 {server} = this.props;
const addedDate = new Date(server.insertDate * 1000); // Convert from unix timestamp
const buttonText = typeof(this.state.joined) == "string" ? `${Strings.PublicServers.joining}...` : this.state.joined ? Strings.PublicServers.joined : Strings.PublicServers.join;
return <div className="bd-server-card" role="button" tabIndex="0" onClick={this.join}>
<div className="bd-server-header">
<div className="bd-server-splash-container"><img src={server.iconUrl} onError={this.handleError} className="bd-server-splash" /></div>
<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>
const badge = <div className="flowerStarContainer-1QeD-L verified-1Jv_7P background-3Da2vZ rowIcon-2tDEcE" style={{width: "16px", height: "16px"}}>
<svg aria-label="Verified &amp; Partnered" className="flowerStar-2tNFCR" 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-U_a6Yh">
<svg className="icon-3BYlXK" 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>;
}
handleError() {
this.props.server.iconUrl = this.props.defaultAvatar();
}
async join() {
if (this.state.joined) return this.props.navigateTo(this.props.server.identifier);
this.setState({joined: "joining"});
const didJoin = await this.props.join(this.props.server.identifier, this.props.server.nativejoin);
this.setState({joined: didJoin});
}
export default function ServerCard({server, joined, join, navigateTo, defaultAvatar}) {
const [isError, setError] = useState(false);
const handleError = useCallback(() => {
setError(true);
}, []);
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";
const {useState, useCallback, useEffect} = React;
function CoreUpdaterPanel(props) {
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"}>
@ -37,88 +39,72 @@ function AddonUpdaterPanel(props) {
</Drawer>;
}
export default class UpdaterPanel extends React.Component {
constructor(props) {
super(props);
export default function UpdaterPanel(props) {
const [hasCoreUpdate, setCoreUpdate] = useState(props.coreUpdater.hasUpdate);
const [updates, setUpdates] = useState({plugins: props.pluginUpdater.pending.slice(0), themes: props.themeUpdater.pending.slice(0)});
this.state = {
hasCoreUpdate: this.props.coreUpdater.hasUpdate,
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;
const checkAddons = useCallback(async (type) => {
const updater = type === "plugins" ? props.pluginUpdater : props.themeUpdater;
await updater.checkAll(false);
this.setState({[type]: updater.pending.slice(0)});
}
setUpdates({...updates, [type]: updater.pending.slice(0)});
}, []);
async updateAddon(type, filename) {
const updater = type === "plugins" ? this.props.pluginUpdater : this.props.themeUpdater;
const update = useCallback(() => {
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);
this.setState(prev => {
setUpdates(prev => {
prev[type].splice(prev[type].indexOf(filename), 1);
return prev;
});
}
}, []);
async updateAllAddons(type) {
const toUpdate = this.state[type].slice(0);
const updateAllAddons = useCallback(async (type) => {
const toUpdate = updates[type].slice(0);
for (const filename of toUpdate) {
await this.updateAddon(type, filename);
await updateAddon(type, filename);
}
}
}, []);
render() {
return [
<SettingsTitle text={Strings.Panels.updates} button={{title: Strings.Updater.checkForUpdates, onClick: this.checkForUpdates}} />,
<CoreUpdaterPanel remoteVersion={this.props.coreUpdater.remoteVersion} hasUpdate={this.state.hasCoreUpdate} update={this.updateCore} />,
<AddonUpdaterPanel type="plugins" pending={this.state.plugins} update={this.updateAddon} updateAll={this.updateAllAddons} updater={this.props.pluginUpdater} />,
<AddonUpdaterPanel type="themes" pending={this.state.themes} update={this.updateAddon} updateAll={this.updateAllAddons} updater={this.props.themeUpdater} />,
];
}
return [
<SettingsTitle text={Strings.Panels.updates} button={{title: Strings.Updater.checkForUpdates, onClick: checkForUpdates}} />,
<CoreUpdaterPanel remoteVersion={props.coreUpdater.remoteVersion} hasUpdate={hasCoreUpdate} update={updateCore} />,
<AddonUpdaterPanel type="plugins" pending={updates.plugins} update={updateAddon} updateAll={updateAllAddons} updater={props.pluginUpdater} />,
<AddonUpdaterPanel type="themes" pending={updates.themes} update={updateAddon} updateAll={updateAllAddons} updater={props.themeUpdater} />,
];
}