Fix Modals, Toasts

* Make external modules be cached
* Fix ConfirmationModal component being found wrong
* Make default modal actually be standalone and be used for react crash fallback
* Fixes toasts showing up in the crash screen
This commit is contained in:
Strencher 2022-10-08 22:28:56 +02:00
parent dd97539da3
commit 32bf2be211
5 changed files with 224 additions and 66 deletions

View File

@ -52,6 +52,8 @@ export default new class Core {
Logger.log("Startup", "Initializing Editor");
await Editor.initialize();
Modals.initialize();
Logger.log("Startup", "Initializing Builtins");
for (const module in Builtins) {
Builtins[module].initialize();
@ -148,4 +150,4 @@ export default new class Core {
});
}
}
};
};

View File

@ -0,0 +1,73 @@
.bd-modal-wrapper {
position: absolute;
z-index: 1000;
width: 100vw;
height: 100vh;
}
.bd-backdrop {
width: 100%;
height: 100%;
background: rgba(0,0,0, .6);
position: absolute;
}
.bd-modal {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 33%;
}
.bd-modal-inner {
background: var(--background-primary);
border-radius: 4px;
overflow: hidden;
animation: bd-modal-open ease-out;
animation-duration: 300ms;
}
.bd-modal-wrapper.closing .bd-modal-inner {
animation: bd-modal-close ease-in;
animation-duration: 300ms;
}
.bd-modal .footer {
display: flex;
justify-content: flex-end;
padding: 15px;
background: var(--background-secondary);
}
.bd-modal-body {
padding: 20px 15px;
padding-top: 0;
}
.bd-modal .header {
padding: 15px;
}
.bd-modal .title {
font-size: 22px;
color: #fff;
font-weight: 600;
}
.bd-modal-body {
color: #fff;
}
.bd-modal .footer .bd-button {
min-width: 80px;
height: 38px;
}
@keyframes bd-modal-close {
to {transform: scale(0.7);}
}
@keyframes bd-modal-open {
from {transform: scale(0.7);}
}

View File

@ -1,26 +1,27 @@
import Logger from "common/logger";
import {React, IPC} from "modules";
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {hasError: false};
}
componentDidCatch() {
this.setState({hasError: true});
}
render() {
if (this.state.hasError) return <div onClick={() => IPC.openDevTools()} className="react-error">There was an unexpected Error. Click to open console for more details.</div>;
return this.props.children;
}
}
const originalRender = ErrorBoundary.prototype.render;
Object.defineProperty(ErrorBoundary.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("ErrorBoundary", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");},
get: () => originalRender
});
import Logger from "common/logger";
import {React, IPC} from "modules";
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {hasError: false};
}
componentDidCatch(error) {
this.setState({hasError: true});
if (typeof this.props.onError === "function") this.props.onError(error);
}
render() {
if (this.state.hasError) return <div onClick={() => IPC.openDevTools()} className="react-error">There was an unexpected Error. Click to open console for more details.</div>;
return this.props.children;
}
}
const originalRender = ErrorBoundary.prototype.render;
Object.defineProperty(ErrorBoundary.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("ErrorBoundary", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");},
get: () => originalRender
});

View File

@ -1,6 +1,6 @@
import {Config} from "data";
import Logger from "common/logger";
import {WebpackModules, React, Settings, Strings, DOMManager, DiscordModules} from "modules";
import {WebpackModules, React, ReactDOM, Settings, Strings, DOMManager, DiscordModules} from "modules";
import FormattableString from "../structs/string";
import AddonErrorModal from "./addonerrormodal";
import ErrorBoundary from "./errorboundary";
@ -11,23 +11,38 @@ export default class Modals {
static get shouldShowAddonErrors() {return Settings.get("settings", "addons", "addonErrors");}
static get ModalActions() {
return {
return this._ModalActions ??= {
openModal: WebpackModules.getModule(m => m?.toString().includes("onCloseCallback") && m?.toString().includes("Layer")),
closeModal: WebpackModules.getModule(m => m?.toString().includes("onCloseCallback()"))
};
}
static get ModalStack() {return WebpackModules.getByProps("push", "update", "pop", "popWithKey");}
static get ModalComponents() {return WebpackModules.getByProps("Header", "Footer");}
static get ModalRoot() {return WebpackModules.getModule(m => m?.toString().includes("ENTERING"));}
static get ModalClasses() {return WebpackModules.getByProps("modal", "content");}
static get FlexElements() {return WebpackModules.getByProps("Child", "Align");}
static get FormTitle() {return WebpackModules.getByProps("Tags", "Sizes");}
static get TextElement() {return WebpackModules.getModule(m => m?.Sizes?.SIZE_32 && m.Colors);}
static get ConfirmationModal() {return WebpackModules.getModule(m => m?.toString()?.includes("confirmText"));}
static get Markdown() {return WebpackModules.find(m => m?.prototype?.render && m.rules);}
static get Buttons() {return WebpackModules.getByProps("BorderColors");}
static get ModalStack() {return this._ModalStack ??= WebpackModules.getByProps("push", "update", "pop", "popWithKey");}
static get ModalComponents() {return this._ModalComponents ??= WebpackModules.getByProps("Header", "Footer");}
static get ModalRoot() {return this._ModalRoot ??= WebpackModules.getModule(m => m?.toString().includes("ENTERING"));}
static get ModalClasses() {return this._ModalClasses ??= WebpackModules.getByProps("modal", "content");}
static get FlexElements() {return this._FlexElements ??= WebpackModules.getByProps("Child", "Align");}
static get FormTitle() {return this._FormTitle ??= WebpackModules.getByProps("Tags", "Sizes");}
static get TextElement() {return this._TextElement ??= WebpackModules.getModule(m => m?.Sizes?.SIZE_32 && m.Colors);}
static get ConfirmationModal() {return this._ConfirmationModal ??= WebpackModules.getModule(m => m?.toString()?.includes(".confirmButtonColor"));}
static get Markdown() {return this._Markdown ??= WebpackModules.find(m => m?.prototype?.render && m.rules);}
static get Buttons() {return this._Buttons ??= WebpackModules.getByProps("BorderColors");}
static get ModalQueue() {return this._ModalQueue ??= [];}
static get hasModalOpen() {return !!document.getElementsByClassName("bd-modal").length;}
static default(title, content) {
static async initialize() {
const names = ["ModalActions", "Markdown", "ModalRoot", "ModalComponents", "Buttons", "TextElement", "FlexElements"];
for (const name of names) {
const value = this[name];
if (!value) {
Logger.warn("Modals", `Missing ${name} module!`);
}
}
}
static default(title, content, buttons = []) {
const modal = DOMManager.parseHTML(`<div class="bd-modal-wrapper theme-dark">
<div class="bd-backdrop backdrop-1wrmKB"></div>
<div class="bd-modal modal-1UGdnR">
@ -37,26 +52,77 @@ export default class Modals {
</div>
<div class="bd-modal-body">
<div class="scroller-wrap fade">
<div class="scroller">
${content}
</div>
<div class="scroller"></div>
</div>
</div>
<div class="footer footer-2yfCgX footer-3rDWdC footer-2gL1pp">
<button type="button" class="bd-button">${Strings.Modals.okay}</button>
</div>
<div class="footer footer-2yfCgX footer-3rDWdC footer-2gL1pp"></div>
</div>
</div>
</div>`);
modal.querySelector(".footer button").addEventListener("click", () => {
const handleClose = () => {
modal.classList.add("closing");
setTimeout(() => {modal.remove();}, 300);
});
modal.querySelector(".bd-backdrop").addEventListener("click", () => {
modal.classList.add("closing");
setTimeout(() => {modal.remove();}, 300);
});
document.querySelector("#app-mount").append(modal);
setTimeout(() => {
modal.remove();
const next = this.ModalQueue.shift();
if (!next) return;
next();
}, 300);
};
if (!buttons.length) {
buttons.push({
label: Strings.Modals.okay,
action: handleClose
});
}
const buttonContainer = modal.querySelector(".footer");
for (const button of buttons) {
const buttonEl = Object.assign(document.createElement("button"), {
onclick: (e) => {
try {button.action(e);} catch (error) {console.error(error);}
handleClose();
},
type: "button",
className: "bd-button"
});
if (button.danger) buttonEl.classList.add("bd-button-danger")
buttonEl.append(button.label);
buttonContainer.appendChild(buttonEl);
}
if (Array.isArray(content) ? content.every(el => React.isValidElement(el)) : React.isValidElement(content)) {
const container = modal.querySelector(".scroller");
try {
ReactDOM.render(content, container);
} catch (error) {
container.append(DOMManager.parseHTML(`<span style="color: red">There was an unexpected error. Modal could not be rendered.</span>`));
}
DOMManager.onRemoved(container, () => {
ReactDOM.unmountComponentAtNode(container);
});
} else {
modal.querySelector(".scroller").append(content);
}
modal.querySelector(".footer button").addEventListener("click", handleClose);
modal.querySelector(".bd-backdrop").addEventListener("click", handleClose);
const handleOpen = () => document.getElementById("app-mount").append(modal);
if (this.hasModalOpen) {
this.ModalQueue.push(handleOpen);
} else {
handleOpen();
}
}
static alert(title, content) {
@ -80,25 +146,41 @@ export default class Modals {
const Markdown = this.Markdown;
const ConfirmationModal = this.ConfirmationModal;
const ModalActions = this.ModalActions;
if (content instanceof FormattableString) content = content.toString();
if (!this.ModalActions || !this.ConfirmationModal || !this.Markdown) return this.default(title, content);
const emptyFunction = () => {};
const {onConfirm = emptyFunction, onCancel = emptyFunction, confirmText = Strings.Modals.okay, cancelText = Strings.Modals.cancel, danger = false, key = undefined} = options;
if (!this.ModalActions || !this.ConfirmationModal || !this.Markdown) return this.default(title, content, [
confirmText && {label: confirmText, action: onConfirm},
cancelText && {label: cancelText, action: onCancel, danger}
].filter(Boolean));
if (!Array.isArray(content)) content = [content];
content = content.map(c => typeof(c) === "string" ? React.createElement(Markdown, null, c) : c);
return ModalActions.openModal(props => {
return React.createElement(ConfirmationModal, Object.assign({
let modalKey = ModalActions.openModal(props => {
return React.createElement(ErrorBoundary, {
onError: () => {
setTimeout(() => {
ModalActions.closeModal(modalKey);
this.default(title, content, [
confirmText && {label: confirmText, action: onConfirm},
cancelText && {label: cancelText, action: onCancel, danger}
].filter(Boolean));
});
}
}, React.createElement(ConfirmationModal, Object.assign({
header: title,
confirmButtonColor: danger ? this.Buttons.Colors.RED : this.Buttons.Colors.BRAND,
confirmText: confirmText,
cancelText: cancelText,
onConfirm: onConfirm,
onCancel: onCancel
}, props), content);
}, props), React.createElement(ErrorBoundary, {}, content)));
}, {modalKey: key});
return modalKey;
}
static showAddonErrors({plugins: pluginErrors = [], themes: themeErrors = []}) {
@ -110,7 +192,7 @@ export default class Modals {
}
this.addonErrorsRef = React.createRef();
this.ModalActions.openModal(props => React.createElement(this.ModalRoot, Object.assign(props, {
this.ModalActions.openModal(props => React.createElement(ErrorBoundary, null, React.createElement(this.ModalRoot, Object.assign(props, {
size: "medium",
className: "bd-error-modal",
children: [
@ -127,7 +209,7 @@ export default class Modals {
className: "bd-button"
}, Strings.Modals.okay))
]
})));
}))));
}
static showChangelogModal(options = {}) {
@ -179,14 +261,14 @@ export default class Modals {
const originalRoot = OriginalModalClasses.root;
if (originalRoot) OriginalModalClasses.root = `${originalRoot} bd-changelog-modal`;
const key = ModalActions.openModal(props => {
return React.createElement(Changelog, Object.assign({
return React.createElement(ErrorBoundary, null, React.createElement(Changelog, Object.assign({
className: `bd-changelog ${ChangelogClasses.container}`,
selectable: true,
onScroll: _ => _,
onClose: _ => _,
renderHeader: renderHeader,
renderFooter: renderFooter,
}, props), changelogItems);
}, props), changelogItems));
});
const closeModal = ModalActions.closeModal;
@ -241,7 +323,7 @@ export default class Modals {
};
return this.ModalActions.openModal(props => {
return React.createElement(modal, props);
return React.createElement(ErrorBoundary, null, React.createElement(modal, props));
});
}
}
}

View File

@ -64,7 +64,7 @@ export default class Toasts {
const form = container ? container.querySelector("form") : null;
const left = container ? container.getBoundingClientRect().left : 310;
const right = memberlist ? memberlist.getBoundingClientRect().left : 0;
const width = right ? right - container.getBoundingClientRect().left : container.offsetWidth;
const width = right ? right - container.getBoundingClientRect().left : (container?.offsetWidth ?? document.body.offsetWidth / 2);
const bottom = form ? form.offsetHeight : 80;
const toastWrapper = document.createElement("div");
toastWrapper.classList.add("bd-toasts");
@ -73,4 +73,4 @@ export default class Toasts {
toastWrapper.style.setProperty("bottom", bottom + "px");
DOMManager.bdBody.appendChild(toastWrapper);
}
}
}