From 9828b407e1e3f409e6765e294d33ab986427281a Mon Sep 17 00:00:00 2001 From: Haneul Seong Date: Sat, 28 Nov 2020 23:28:18 +0700 Subject: [PATCH] Push Push --- 1XenoLib.plugin.js | 1997 ++++++++++++ BetterImageViewer.plugin.js | 2222 ++++++++++++++ BetterTypingUsers.plugin.js | 463 +++ BetterUnavailableGuilds.plugin.js | 522 ++++ CrashRecovery.plugin.js | 529 ++++ InAppNotifications.plugin.js | 616 ++++ MessageLoggerV2.plugin.js | 4692 +++++++++++++++++++++++++++++ README.md | 7 + UnreadBadgesRedux.plugin.js | 609 ++++ 9 files changed, 11657 insertions(+) create mode 100644 1XenoLib.plugin.js create mode 100644 BetterImageViewer.plugin.js create mode 100644 BetterTypingUsers.plugin.js create mode 100644 BetterUnavailableGuilds.plugin.js create mode 100644 CrashRecovery.plugin.js create mode 100644 InAppNotifications.plugin.js create mode 100644 MessageLoggerV2.plugin.js create mode 100644 README.md create mode 100644 UnreadBadgesRedux.plugin.js diff --git a/1XenoLib.plugin.js b/1XenoLib.plugin.js new file mode 100644 index 0000000..2deeae2 --- /dev/null +++ b/1XenoLib.plugin.js @@ -0,0 +1,1997 @@ +//META{"name":"XenoLib","source":"https://gitdab.com/hana/lightcock/src/branch/master/1XenoLib.plugin.js/","authorId":"287977955240706060","invite":"9jDgyVjTVX","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); + +@else@*/ +/* + * Copyright © 2019-2020, _Lighty_ + * All rights reserved. + * Code may not be redistributed, modified or otherwise taken without explicit permission. + */ +module.exports = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'XenoLib', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: 'LightyPon', + twitter_username: '' + } + ], + version: '1.3.32', + description: 'Simple library to complement plugins with shared code without lowering performance. Also adds needed buttons to some plugins.', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js' + }, + changelog: [ + { + title: '#justblamezeretf', + type: 'fixed', + items: ['Forcefully fixed a few zeres lib issues.', 'Settings on my plugins should now work (as long as they\'re up to date).'] + } + ], + defaultConfig: [ + { + type: 'category', + id: 'notifications', + name: 'Notification settings', + collapsible: true, + shown: true, + settings: [ + { + name: 'Notification position', + id: 'position', + type: 'position', + value: 'topRight' + }, + { + name: 'Notifications use backdrop-filter', + id: 'backdrop', + type: 'switch', + value: true + }, + { + name: 'Background color', + id: 'backdropColor', + type: 'color', + value: '#3e4346', + options: { + defaultColor: '#3e4346' + } + }, + { + name: 'Timeout resets to 0 when hovered', + id: 'timeoutReset', + type: 'switch', + value: true + } + ] + }, + { + type: 'category', + id: 'addons', + name: 'AddonCard settings', + collapsible: true, + shown: false, + settings: [ + { + name: 'Add extra buttons to specific plugins', + note: 'Disabling this will move the buttons to the bottom of plugin settings (if available)', + id: 'extra', + type: 'switch', + value: true + } + ] + } + ] + }; + + /* Build */ + const buildPlugin = ([Plugin, Api]) => { + const { ContextMenu, EmulatedTooltip, Toasts, Settings, Popouts, Modals, Utilities, WebpackModules, Filters, DiscordModules, ColorConverter, DOMTools, DiscordClasses, DiscordSelectors, ReactTools, ReactComponents, DiscordAPI, Logger, PluginUpdater, PluginUtilities, DiscordClassModules, Structs } = Api; + const { React, ModalStack, ContextMenuActions, ContextMenuItem, ContextMenuItemsGroup, ReactDOM, ChannelStore, GuildStore, UserStore, DiscordConstants, Dispatcher, GuildMemberStore, GuildActions, PrivateChannelActions, LayerManager, InviteActions, FlexChild, Titles, Changelog: ChangelogModal } = DiscordModules; + + PluginUpdater.checkForUpdate(config.info.name, config.info.version, config.info.github_raw); + + let CancelledAsync = false; + const DefaultLibrarySettings = {}; + + for (let s = 0; s < config.defaultConfig.length; s++) { + const current = config.defaultConfig[s]; + if (current.type != 'category') { + DefaultLibrarySettings[current.id] = current.value; + } else { + DefaultLibrarySettings[current.id] = {}; + for (let s = 0; s < current.settings.length; s++) { + const subCurrent = current.settings[s]; + DefaultLibrarySettings[current.id][subCurrent.id] = subCurrent.value; + } + } + } + + if (global.XenoLib) { + try { + global.XenoLib.shutdown(); + } catch (e) { } + } + const XenoLib = {}; + XenoLib.shutdown = () => { + try { + Patcher.unpatchAll(); + } catch (e) { + Logger.stacktrace('Failed to unpatch all', e); + } + CancelledAsync = true; + PluginUtilities.removeStyle('XenoLib-CSS'); + try { + const notifWrapper = document.querySelector('.xenoLib-notifications'); + if (notifWrapper) { + ReactDOM.unmountComponentAtNode(notifWrapper); + notifWrapper.remove(); + } + } catch (e) { + Logger.stacktrace('Failed to unmount Notifications component', e); + } + }; + + XenoLib._ = XenoLib.DiscordUtils = WebpackModules.getByProps('bindAll', 'debounce'); + + XenoLib.loadData = (name, key, defaultData, returnNull) => { + try { + return XenoLib._.mergeWith(defaultData ? Utilities.deepclone(defaultData) : {}, BdApi.getData(name, key), (_, b) => { + if (XenoLib._.isArray(b)) return b; + }); + } catch (err) { + Logger.err(name, 'Unable to load data: ', err); + if (returnNull) return null; + return Utilities.deepclone(defaultData); + } + }; + + XenoLib.getClass = (arg, thrw) => { + try { + const args = arg.split(' '); + return WebpackModules.getByProps(...args)[args[args.length - 1]]; + } catch (e) { + if (thrw) throw e; + if (!XenoLib.getClass.__warns[arg] || Date.now() - XenoLib.getClass.__warns[arg] > 1000 * 60) { + Logger.stacktrace(`Failed to get class with props ${arg}`, e); + XenoLib.getClass.__warns[arg] = Date.now(); + } + return ''; + } + }; + XenoLib.getSingleClass = (arg, thrw) => { + try { + return XenoLib.getClass(arg, thrw).split(' ')[0]; + } catch (e) { + if (thrw) throw e; + if (!XenoLib.getSingleClass.__warns[arg] || Date.now() - XenoLib.getSingleClass.__warns[arg] > 1000 * 60) { + Logger.stacktrace(`Failed to get class with props ${arg}`, e); + XenoLib.getSingleClass.__warns[arg] = Date.now(); + } + return ''; + } + }; + XenoLib.getClass.__warns = {}; + XenoLib.getSingleClass.__warns = {}; + + XenoLib.createSmartPatcher = patcher => { + const createPatcher = patcher => { + return (moduleToPatch, functionName, callback, options = {}) => { + try { + var origDef = moduleToPatch[functionName]; + } catch (_) { + return Logger.error(`Failed to patch ${functionName}`); + } + const unpatches = []; + unpatches.push(patcher(moduleToPatch, functionName, callback, options)); + try { + if (origDef.__isBDFDBpatched && moduleToPatch.BDFDBpatch && typeof moduleToPatch.BDFDBpatch[functionName].originalMethod === 'function') { + /* do NOT patch a patch by ZLIb, that'd be bad and cause double items in context menus */ + if ((Utilities.getNestedProp(ZeresPluginLibrary, 'Patcher.patches') || []).findIndex(e => e.module === moduleToPatch) !== -1 && moduleToPatch.BDFDBpatch[functionName].originalMethod.__originalFunction) return; + unpatches.push(patcher(moduleToPatch.BDFDBpatch[functionName], 'originalMethod', callback, options)); + } + } catch (err) { + Logger.stacktrace('Failed to patch BDFDB patches', err); + } + return function unpatch() { + unpatches.forEach(e => e()); + }; + }; + }; + return Object.assign({}, patcher, { + before: createPatcher(patcher.before), + instead: createPatcher(patcher.instead), + after: createPatcher(patcher.after) + }); + }; + + const Patcher = XenoLib.createSmartPatcher(Api.Patcher); + + const LibrarySettings = XenoLib.loadData(config.info.name, 'settings', DefaultLibrarySettings); + + PluginUtilities.addStyle( + 'XenoLib-CSS', + ` + .xenoLib-color-picker .xenoLib-button { + width: 34px; + min-height: 38px; + } + .xenoLib-color-picker .xenoLib-button:hover { + width: 128px; + } + .xenoLib-color-picker .xenoLib-button .${XenoLib.getSingleClass('recording text')} { + opacity: 0; + transform: translate3d(200%,0,0); + } + .xenoLib-color-picker .xenoLib-button:hover .${XenoLib.getSingleClass('recording text')} { + opacity: 1; + transform: translateZ(0); + } + .xenoLib-button-icon { + left: 50%; + top: 50%; + position: absolute; + margin-left: -12px; + margin-top: -8px; + width: 24px; + height: 24px; + opacity: 1; + transform: translateZ(0); + transition: opacity .2s ease-in-out,transform .2s ease-in-out,-webkit-transform .2s ease-in-out; + } + .xenoLib-button-icon.xenoLib-revert > svg { + width: 24px; + height: 24px; + } + .xenoLib-button-icon.xenoLib-revert { + margin-top: -12px; + } + .xenoLib-button:hover .xenoLib-button-icon { + opacity: 0; + transform: translate3d(-200%,0,0); + } + .xenoLib-notifications { + position: absolute; + color: white; + width: 100%; + min-height: 100%; + display: flex; + flex-direction: column; + z-index: 1000; + pointer-events: none; + font-size: 14px; + } + .xenoLib-notification { + min-width: 200px; + overflow: hidden; + } + .xenoLib-notification-content-wrapper { + padding: 22px 20px 0 20px; + } + .xenoLib-centering-bottomLeft .xenoLib-notification-content-wrapper:first-of-type, .xenoLib-centering-bottomMiddle .xenoLib-notification-content-wrapper:first-of-type, .xenoLib-centering-bottomRight .xenoLib-notification-content-wrapper:first-of-type { + padding: 0 20px 20px 20px; + } + .xenoLib-notification-content { + padding: 12px; + overflow: hidden; + background: #474747; + pointer-events: all; + position: relative; + width: 20vw; + white-space: break-spaces; + min-width: 330px; + } + .xenoLib-notification-loadbar { + position: absolute; + bottom: 0; + left: 0px; + width: auto; + background-image: linear-gradient(130deg,var(--grad-one),var(--grad-two)); + height: 5px; + } + .xenoLib-notification-loadbar-user { + animation: fade-loadbar-animation 1.5s ease-in-out infinite; + } + @keyframes fade-loadbar-animation { + 0% { + filter: brightness(75%) + } + 50% { + filter: brightness(100%) + } + to { + filter: brightness(75%) + } + } + .xenoLib-notification-loadbar-striped:before { + content: ""; + position: absolute; + width: 100%; + height: 100%; + border-radius: 5px; + background: linear-gradient( + -20deg, + transparent 35%, + var(--bar-color) 35%, + var(--bar-color) 70%, + transparent 70% + ); + animation: shift 1s linear infinite; + background-size: 60px 100%; + box-shadow: inset 0 0px 1px rgba(0, 0, 0, 0.2), + inset 0 -2px 1px rgba(0, 0, 0, 0.2); + } + @keyframes shift { + to { + background-position: 60px 100%; + } + } + .xenoLib-notification-close { + float: right; + padding: 0; + height: unset; + opacity: .7; + } + .xenLib-notification-counter { + float: right; + margin-top: 2px; + } + .topMiddle-xenoLib { + top: 0; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + } + .bottomMiddle-xenoLib { + bottom: 0; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + } + .xenoLib-centering-topLeft, .xenoLib-centering-bottomLeft { + align-items: flex-start; + } + .xenoLib-centering-topMiddle, .xenoLib-centering-bottomMiddle { + align-items: center; + } + .xenoLib-centering-topRight, .xenoLib-centering-bottomRight { + align-items: flex-end; + } + .xenoLib-centering-bottomLeft, .xenoLib-centering-bottomMiddle, .xenoLib-centering-bottomRight { + flex-direction: column-reverse; + bottom: 0; + } + .XL-chl-p img{ + width: unset !important; + } + .xenoLib-error-text { + padding-top: 5px; + } + ` + ); + + XenoLib.joinClassNames = WebpackModules.getModule(e => e.default && e.default.default); + XenoLib.authorId = '239513071272329217'; + XenoLib.supportServerId = '389049952732446731'; + + try { + const getUserAsync = WebpackModules.getByProps('getUser', 'acceptAgreements').getUser; + const requestUser = () => + getUserAsync(XenoLib.authorId) + .then(user => (XenoLib.author = user)) + .catch(() => setTimeout(requestUser, 1 * 60 * 1000)); + if (UserStore.getUser(XenoLib.authorId)) XenoLib.author = UserStore.getUser(XenoLib.authorId); + else requestUser(); + } catch (e) { + Logger.stacktrace('Failed to grab author object', e); + } + + XenoLib.ReactComponents = {}; + + XenoLib.ReactComponents.ErrorBoundary = class XLErrorBoundary extends React.PureComponent { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + componentDidCatch(err, inf) { + Logger.err(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`); + this.setState({ hasError: true }); + if (typeof this.props.onError === 'function') this.props.onError(err); + } + render() { + if (this.state.hasError) return null; + return this.props.children; + } + }; + + const deprecateFunction = (name, advice, ret = undefined) => () => (Logger.warn(`XenoLib.${name} is deprecated! ${advice}`), ret); + + XenoLib.patchContext = deprecateFunction('patchContext', 'Do manual patching of context menus instead.'); + + const CTXMenu = WebpackModules.getByProps('default', 'MenuStyle'); + + class ContextMenuWrapper extends React.PureComponent { + constructor(props) { + super(props); + this.handleOnClose = this.handleOnClose.bind(this); + } + handleOnClose() { + ContextMenuActions.closeContextMenu(); + if (this.props.target instanceof HTMLElement) this.props.target.focus(); + } + render() { + return React.createElement(CTXMenu.default, { onClose: this.handleOnClose, id: 'xenolib-context' }, this.props.menu); + } + } + XenoLib.createSharedContext = (element, menuCreation) => { + if (element.__XenoLib_ContextMenus) { + element.__XenoLib_ContextMenus.push(menuCreation); + } else { + element.__XenoLib_ContextMenus = [menuCreation]; + const oOnContextMenu = element.props.onContextMenu; + element.props.onContextMenu = e => (typeof oOnContextMenu === 'function' && oOnContextMenu(e), ContextMenuActions.openContextMenu(e, _ => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'CTX Menu' }, React.createElement(ContextMenuWrapper, { menu: element.__XenoLib_ContextMenus.map(m => m()), ..._ })))); + } + }; + + const contextMenuItems = WebpackModules.find(m => m.MenuRadioItem && !m.default); + XenoLib.unpatchContext = deprecateFunction('unpatchContext', 'Manual patching needs manual unpatching'); + XenoLib.createContextMenuItem = (label, action, id, options = {}) => (!contextMenuItems ? null : React.createElement(contextMenuItems.MenuItem, { label, id, action: () => (!options.noClose && ContextMenuActions.closeContextMenu(), action()), ...options })); + XenoLib.createContextMenuSubMenu = (label, children, id, options = {}) => (!contextMenuItems ? null : React.createElement(contextMenuItems.MenuItem, { label, children, id, ...options })); + XenoLib.createContextMenuGroup = (children, options) => (!contextMenuItems ? null : React.createElement(contextMenuItems.MenuGroup, { children, ...options })); + + try { + XenoLib.ReactComponents.ButtonOptions = WebpackModules.getByProps('ButtonLink'); + XenoLib.ReactComponents.Button = XenoLib.ReactComponents.ButtonOptions.default; + } catch (e) { + Logger.stacktrace('Error getting Button component', e); + } + + function patchAddonCardAnyway(manualPatch) { + try { + if (patchAddonCardAnyway.patched) return; + patchAddonCardAnyway.patched = true; + const LinkClassname = XenoLib.joinClassNames(XenoLib.getClass('anchorUnderlineOnHover anchor'), XenoLib.getClass('anchor anchorUnderlineOnHover'), 'bda-author'); + const handlePatch = (_this, _, ret) => { + if (!_this.props.addon || !_this.props.addon.plugin || typeof _this.props.addon.plugin.getAuthor().indexOf('Lighty') === -1) return; + const settingsProps = Utilities.findInReactTree(ret, e => e && e.className === 'plugin-settings'); + if (settingsProps) delete settingsProps.id; + const author = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf('bda-author') !== -1); + if (!author || typeof author.props.children !== 'string' || author.props.children.indexOf('Lighty') === -1) return; + const onClick = () => { + if (DiscordAPI.currentUser.id === XenoLib.authorId) return; + PrivateChannelActions.ensurePrivateChannel(DiscordAPI.currentUser.id, XenoLib.authorId).then(() => { + PrivateChannelActions.openPrivateChannel(DiscordAPI.currentUser.id, XenoLib.authorId); + LayerManager.popLayer(); + }); + }; + if (author.props.children === 'Lighty') { + author.type = 'a'; + author.props.className = LinkClassname; + author.props.onClick = onClick; + } else { + const idx = author.props.children.indexOf('Lighty'); + const pre = author.props.children.slice(0, idx); + const post = author.props.children.slice(idx + 6); + author.props.children = [ + pre, + React.createElement( + 'a', + { + className: LinkClassname, + onClick + }, + 'Lighty' + ), + post + ]; + delete author.props.onClick; + author.props.className = 'bda-author'; + author.type = 'span'; + } + let footerProps = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf('bda-links') !== -1); + if (!footerProps) return; + footerProps = footerProps.props; + if (!Array.isArray(footerProps.children)) footerProps.children = [footerProps.children]; + const findLink = name => Utilities.findInReactTree(footerProps.children, e => e && e.props && e.props.children === name); + const websiteLink = findLink('Website'); + const sourceLink = findLink('Source'); + const supportServerLink = findLink('Support Server'); + footerProps.children = []; + if (websiteLink) { + const href = websiteLink.props.href; + delete websiteLink.props.href; + delete websiteLink.props.target; + websiteLink.props.onClick = () => window.open(href); + footerProps.children.push(websiteLink); + } + if (sourceLink) { + const href = sourceLink.props.href; + delete sourceLink.props.href; + delete sourceLink.props.target; + sourceLink.props.onClick = () => window.open(href); + footerProps.children.push(websiteLink ? ' | ' : null, sourceLink); + } + footerProps.children.push(websiteLink || sourceLink ? ' | ' : null, React.createElement('a', { className: 'bda-link bda-link-website', onClick: e => ContextMenuActions.openContextMenu(e, e => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'Donate button CTX menu' }, React.createElement(ContextMenuWrapper, { menu: XenoLib.createContextMenuGroup([XenoLib.createContextMenuItem('Paypal', () => window.open('https://paypal.me/lighty13'), 'paypal'), XenoLib.createContextMenuItem('Ko-fi', () => window.open('https://ko-fi.com/lighty_'), 'kofi'), XenoLib.createContextMenuItem('Patreon', () => window.open('https://www.patreon.com/lightyp'), 'patreon')]), ...e }))) }, 'Donate')); + footerProps.children.push(' | ', supportServerLink || React.createElement('a', { className: 'bda-link bda-link-website', onClick: () => (LayerManager.popLayer(), InviteActions.acceptInviteAndTransitionToInviteChannel('NYvWdN5')) }, 'Support Server')); + footerProps.children.push(' | ', React.createElement('a', { className: 'bda-link bda-link-website', onClick: () => (_this.props.addon.plugin.showChangelog ? _this.props.addon.plugin.showChangelog() : Modals.showChangelogModal(_this.props.addon.plugin.getName() + ' Changelog', _this.props.addon.plugin.getVersion(), _this.props.addon.plugin.getChanges())) }, 'Changelog')); + footerProps = null; + }; + async function patchRewriteCard() { + const component = [...ReactComponents.components.entries()].find(([_, e]) => e.component && e.component.prototype && e.component.prototype.reload && e.component.prototype.showSettings); + const AddonCard = component ? component[1] : await ReactComponents.getComponent('AddonCard', '.bda-slist > .ui-switch-item', e => e.prototype && e.prototype.reload && e.prototype.showSettings); + if (CancelledAsync) return; + const ContentColumn = await ReactComponents.getComponent('ContentColumn', '.content-column'); + class PatchedAddonCard extends AddonCard.component { + render() { + const ret = super.render(); + try { + /* did I mention I am Lighty? */ + handlePatch(this, undefined, ret); + } catch (err) { + Logger.stacktrace('AddonCard patch', err); + } + return ret; + } + } + let firstRender = true; + Patcher.after(ContentColumn.component.prototype, 'render', (_, __, ret) => { + if (!LibrarySettings.addons.extra) return; + const list = Utilities.findInReactTree(ret, e => e && typeof e.className === 'string' && e.className.indexOf('bd-addon-list') !== -1); + if (Utilities.getNestedProp(list, 'children.0.props.children.type') !== AddonCard.component) return; + for (const item of list.children) { + const card = Utilities.getNestedProp(item, 'props.children'); + if (!card) continue; + card.type = PatchedAddonCard; + } + if (!firstRender) return; + ret.key = DiscordModules.KeyGenerator(); + firstRender = false; + }); + if (manualPatch) return; + ContentColumn.forceUpdateAll(); + AddonCard.forceUpdateAll(); + } + patchRewriteCard(); + } catch (e) { + Logger.stacktrace('Failed to patch V2C_*Card or AddonCard (BBD rewrite)', e); + } + } + if (LibrarySettings.addons.extra) patchAddonCardAnyway(); + + try { + XenoLib.ReactComponents.PluginFooter = class XLPluginFooter extends React.PureComponent { + render() { + if (LibrarySettings.addons.extra) return null; + return React.createElement( + 'div', + { + style: { + display: 'flex' + } + }, + React.createElement( + XenoLib.ReactComponents.Button, + { + style: { + flex: '2 1 auto' + }, + onClick: this.props.showChangelog + }, + 'Changelog' + ), + React.createElement( + XenoLib.ReactComponents.Button, + { + style: { + flex: '2 1 auto' + }, + onClick: e => ContextMenuActions.openContextMenu(e, e => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'Donate button CTX menu' }, React.createElement(ContextMenuWrapper, { menu: XenoLib.createContextMenuGroup([XenoLib.createContextMenuItem('Paypal', () => window.open('https://paypal.me/lighty13'), 'paypal'), XenoLib.createContextMenuItem('Ko-fi', () => window.open('https://ko-fi.com/lighty_'), 'kofi'), XenoLib.createContextMenuItem('Patreon', () => window.open('https://www.patreon.com/lightyp'), 'patreon')]), ...e }))) + }, + 'Donate' + ), + React.createElement( + XenoLib.ReactComponents.Button, + { + style: { + flex: '2 1 auto' + }, + onClick: () => (LayerManager.popLayer(), InviteActions.acceptInviteAndTransitionToInviteChannel('NYvWdN5')) + }, + 'Support server' + ) + ); + } + }; + } catch (err) { + Logger.stacktrace('Error creating plugin footer'); + XenoLib.ReactComponents.PluginFooter = DiscordConstants.NOOP_NULL; + } + + const TextElement = WebpackModules.getByDisplayName('Text'); + + /* shared between FilePicker and ColorPicker */ + const MultiInputClassname = XenoLib.joinClassNames(Utilities.getNestedProp(DiscordClasses, 'BasicInputs.input.value'), XenoLib.getClass('multiInput')); + const MultiInputFirstClassname = XenoLib.getClass('multiInputFirst'); + const MultiInputFieldClassname = XenoLib.getClass('multiInputField'); + const ErrorMessageClassname = XenoLib.joinClassNames('xenoLib-error-text', XenoLib.getClass('errorMessage'), Utilities.getNestedProp(TextElement, 'Colors.ERROR')); + const ErrorClassname = XenoLib.getClass('input error'); + + try { + const DelayedCall = WebpackModules.getByProps('DelayedCall').DelayedCall; + const FsModule = require('fs'); + /** + * @interface + * @name module:FilePicker + * @property {string} path + * @property {string} placeholder + * @property {Function} onChange + * @property {object} properties + * @property {bool} nullOnInvalid + * @property {bool} saveOnEnter + */ + XenoLib.ReactComponents.FilePicker = class FilePicker extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + multiInputFocused: false, + path: props.path, + error: null + }; + XenoLib._.bindAll(this, ['handleOnBrowse', 'handleChange', 'checkInvalidDir']); + this.handleKeyDown = XenoLib._.throttle(this.handleKeyDown.bind(this), 500); + this.delayedCallVerifyPath = new DelayedCall(500, () => this.checkInvalidDir()); + } + checkInvalidDir(doSave) { + FsModule.access(this.state.path, FsModule.constants.W_OK, error => { + const invalid = (error && error.message.match(/.*: (.*), access '/)[1]) || null; + this.setState({ error: invalid }); + if (this.props.saveOnEnter && !doSave) return; + if (invalid) this.props.onChange(this.props.nullOnInvalid ? null : ''); + else this.props.onChange(this.state.path); + }); + } + handleOnBrowse() { + DiscordNative.fileManager.showOpenDialog({ title: this.props.title, properties: this.props.properties }).then(({ filePaths: [path] }) => { + if (path) this.handleChange(path); + }); + } + handleChange(path) { + this.setState({ path }); + this.delayedCallVerifyPath.delay(); + } + handleKeyDown(e) { + if (!this.props.saveOnEnter || e.which !== DiscordConstants.KeyboardKeys.ENTER) return; + this.checkInvalidDir(true); + } + render() { + const n = {}; + n[DiscordClasses.BasicInputs.focused] = this.state.multiInputFocused; + n[ErrorClassname] = !!this.state.error; + return React.createElement( + 'div', + { className: DiscordClasses.BasicInputs.inputWrapper, style: { width: '100%' } }, + React.createElement( + 'div', + { className: XenoLib.joinClassNames(MultiInputClassname, n) }, + React.createElement(DiscordModules.Textbox, { + value: this.state.path, + placeholder: this.props.placeholder, + onChange: this.handleChange, + onFocus: () => this.setState({ multiInputFocused: true }), + onBlur: () => this.setState({ multiInputFocused: false }), + onKeyDown: this.handleKeyDown, + autoFocus: false, + className: MultiInputFirstClassname, + inputClassName: MultiInputFieldClassname + }), + React.createElement(XenoLib.ReactComponents.Button, { onClick: this.handleOnBrowse, color: (!!this.state.error && XenoLib.ReactComponents.ButtonOptions.ButtonColors.RED) || XenoLib.ReactComponents.ButtonOptions.ButtonColors.GREY, look: XenoLib.ReactComponents.ButtonOptions.ButtonLooks.GHOST, size: XenoLib.ReactComponents.Button.Sizes.MEDIUM }, 'Browse') + ), + !!this.state.error && React.createElement('div', { className: ErrorMessageClassname }, 'Error: ', this.state.error) + ); + } + }; + } catch (e) { + Logger.stacktrace('Failed to create FilePicker component', e); + } + + /** + * @param {string} name - name label of the setting + * @param {string} note - help/note to show underneath or above the setting + * @param {string} value - current hex color + * @param {callable} onChange - callback to perform on setting change, callback receives hex string + * @param {object} [options] - object of options to give to the setting + * @param {boolean} [options.disabled=false] - should the setting be disabled + * @param {Array} [options.colors=presetColors] - preset list of colors + * @author Zerebos, from his library ZLibrary + */ + const FormItem = WebpackModules.getByDisplayName('FormItem'); + const DeprecatedModal = WebpackModules.getByDisplayName('DeprecatedModal'); + + const ColorPickerComponent = (_ => { + try { + const GuildSettingsRoles = WebpackModules.getByDisplayName('FluxContainer(GuildSettingsRoles)'); + const RoleSettingsContainer = GuildSettingsRoles.prototype.render.call({ + memoizedGetStateFromStores: _ => { } + }).type.prototype.renderRoleSettings.call({ + props: { + guild: { + id: '', + isOwner: _ => false + } + }, + getSelectedRole: _ => ({ id: '' }), + renderHeader: _ => null + }); + const RoleSettings = Utilities.findInReactTree(RoleSettingsContainer, e => e && e.type && e.type.displayName === "GuildRoleSettings").type.prototype.renderColorPicker.call({ + props: { + role: {} + } + }); + return RoleSettings.props.children.type; + } catch (err) { + Logger.stacktrace('Failed to get lazy colorpicker, unsurprisingly', err); + return _ => null; + } + })() + + class ColorPickerModal extends React.PureComponent { + constructor(props) { + super(props); + this.state = { value: props.value }; + XenoLib._.bindAll(this, ['handleChange']); + } + handleChange(value) { + this.setState({ value }); + this.props.onChange(ColorConverter.int2hex(value)); + } + render() { + return React.createElement( + DeprecatedModal, + { tag: 'form', onSubmit: this.handleSubmit, size: '' }, + React.createElement( + DeprecatedModal.Content, + {}, + React.createElement( + FormItem, + { className: XenoLib.joinClassNames(DiscordClasses.Margins.marginTop20.value, DiscordClasses.Margins.marginBottom20.value) }, + React.createElement(ColorPickerComponent, { + defaultColor: this.props.defaultColor, + colors: [16711680, 16746496, 16763904, 13434624, 65314, 65484, 61183, 43775, 26367, 8913151, 16711918, 16711782, 11730944, 11755264, 11767552, 9417472, 45848, 45967, 42931, 30643, 18355, 6226099, 11731111, 11731015], + value: this.state.value, + onChange: this.handleChange + }) + ) + ) + ); + } + } + + const ExtraButtonClassname = XenoLib.joinClassNames('xenoLib-button', XenoLib.getClass('recording button')); + const TextClassname = XenoLib.getClass('recording text'); + const DropperIcon = React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16' }, React.createElement('path', { d: 'M14.994 1.006C13.858-.257 11.904-.3 10.72.89L8.637 2.975l-.696-.697-1.387 1.388 5.557 5.557 1.387-1.388-.697-.697 1.964-1.964c1.13-1.13 1.3-2.985.23-4.168zm-13.25 10.25c-.225.224-.408.48-.55.764L.02 14.37l1.39 1.39 2.35-1.174c.283-.14.54-.33.765-.55l4.808-4.808-2.776-2.776-4.813 4.803z', fill: 'currentColor' })); + const ClockReverseIcon = React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24' }, React.createElement('path', { d: 'M13,3 C8.03,3 4,7.03 4,12 L1,12 L4.89,15.89 L4.96,16.03 L9,12 L6,12 C6,8.13 9.13,5 13,5 C16.87,5 20,8.13 20,12 C20,15.87 16.87,19 13,19 C11.07,19 9.32,18.21 8.06,16.94 L6.64,18.36 C8.27,19.99 10.51,21 13,21 C17.97,21 22,16.97 22,12 C22,7.03 17.97,3 13,3 L13,3 Z M12,8 L12,13 L16.28,15.54 L17,14.33 L13.5,12.25 L13.5,8 L12,8 L12,8 Z', fill: 'currentColor' })); + class ColorPicker extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + error: null, + value: props.value, + multiInputFocused: false + }; + XenoLib._.bindAll(this, ['handleChange', 'handleColorPicker', 'handleReset']); + } + handleChange(value) { + if (!value.length) { + this.state.error = 'You must input a hex string'; + } else if (!ColorConverter.isValidHex(value)) { + this.state.error = 'Invalid hex string'; + } else { + this.state.error = null; + } + this.setState({ value }); + this.props.onChange(!value.length || !ColorConverter.isValidHex(value) ? this.props.defaultColor : value); + } + handleColorPicker() { + const modalId = ModalStack.push(e => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'color picker modal', onError: () => ModalStack.popWithKey(modalId) }, React.createElement(ColorPickerModal, { ...e, defaultColor: ColorConverter.hex2int(this.props.defaultColor), value: ColorConverter.hex2int(this.props.value), onChange: this.handleChange }))); + } + handleReset() { + this.handleChange(this.props.defaultColor); + } + render() { + const n = {}; + n[DiscordClasses.BasicInputs.focused] = this.state.multiInputFocused; + n[ErrorClassname] = !!this.state.error; + return React.createElement( + 'div', + { className: XenoLib.joinClassNames(DiscordClasses.BasicInputs.inputWrapper.value, 'xenoLib-color-picker'), style: { width: '100%' } }, + React.createElement( + 'div', + { className: XenoLib.joinClassNames(MultiInputClassname, n) }, + React.createElement('div', { + className: XenoLib.ReactComponents.Button.Sizes.SMALL, + style: { + backgroundColor: this.state.value, + height: 38 + } + }), + React.createElement(DiscordModules.Textbox, { + value: this.state.value, + placeholder: 'Hex color', + onChange: this.handleChange, + onFocus: () => this.setState({ multiInputFocused: true }), + onBlur: () => this.setState({ multiInputFocused: false }), + autoFocus: false, + className: MultiInputFirstClassname, + inputClassName: MultiInputFieldClassname + }), + React.createElement( + XenoLib.ReactComponents.Button, + { + onClick: this.handleColorPicker, + color: (!!this.state.error && XenoLib.ReactComponents.ButtonOptions.ButtonColors.RED) || XenoLib.ReactComponents.ButtonOptions.ButtonColors.GREY, + look: XenoLib.ReactComponents.ButtonOptions.ButtonLooks.GHOST, + size: XenoLib.ReactComponents.Button.Sizes.MIN, + className: ExtraButtonClassname + }, + React.createElement('span', { className: TextClassname }, 'Color picker'), + React.createElement( + 'span', + { + className: 'xenoLib-button-icon' + }, + DropperIcon + ) + ), + React.createElement( + XenoLib.ReactComponents.Button, + { + onClick: this.handleReset, + color: (!!this.state.error && XenoLib.ReactComponents.ButtonOptions.ButtonColors.RED) || XenoLib.ReactComponents.ButtonOptions.ButtonColors.GREY, + look: XenoLib.ReactComponents.ButtonOptions.ButtonLooks.GHOST, + size: XenoLib.ReactComponents.Button.Sizes.MIN, + className: ExtraButtonClassname + }, + React.createElement('span', { className: TextClassname }, 'Reset'), + React.createElement( + 'span', + { + className: 'xenoLib-button-icon xenoLib-revert' + }, + ClockReverseIcon + ) + ) + ), + !!this.state.error && React.createElement('div', { className: ErrorMessageClassname }, 'Error: ', this.state.error) + ); + } + } + XenoLib.Settings = {}; + XenoLib.Settings.FilePicker = class FilePickerSettingField extends Settings.SettingField { + constructor(name, note, value, onChange, options = { properties: ['openDirectory', 'createDirectory'], placeholder: 'Path to folder', defaultPath: '' }) { + super(name, note, onChange, XenoLib.ReactComponents.FilePicker || class b { }, { + onChange: reactElement => path => { + this.onChange(path ? path : options.defaultPath); + }, + path: value, + nullOnInvalid: true, + ...options + }); + } + }; + XenoLib.Settings.ColorPicker = class ColorPickerSettingField extends Settings.SettingField { + constructor(name, note, value, onChange, options = {}) { + super(name, note, onChange, ColorPicker, { + disabled: options.disabled ? true : false, + onChange: reactElement => color => { + this.onChange(color); + }, + defaultColor: typeof options.defaultColor !== 'undefined' ? options.defaultColor : ColorConverter.int2hex(DiscordConstants.DEFAULT_ROLE_COLOR), + value + }); + } + }; + + XenoLib.Settings.PluginFooter = class PluginFooterField extends Settings.SettingField { + constructor(showChangelog) { + super('', '', DiscordConstants.NOOP, XenoLib.ReactComponents.PluginFooter, { + showChangelog + }); + } + }; + + XenoLib.changeName = (currentName, newName) => { + try { + const path = require('path'); + const fs = require('fs'); + const pluginsFolder = path.dirname(currentName); + const pluginName = path.basename(currentName).match(/^[^\.]+/)[0]; + if (pluginName === newName) return true; + const wasEnabled = BdApi.Plugins && BdApi.Plugins.isEnabled ? BdApi.Plugins.isEnabled(pluginName) : global.pluginCookie && pluginCookie[pluginName]; + fs.accessSync(currentName, fs.constants.W_OK | fs.constants.R_OK); + const files = fs.readdirSync(pluginsFolder); + files.forEach(file => { + if (!file.startsWith(pluginName) || file.startsWith(newName) || file.indexOf('.plugin.js') !== -1) return; + fs.renameSync(path.resolve(pluginsFolder, file), path.resolve(pluginsFolder, `${newName}${file.match(new RegExp(`^${pluginName}(.*)`))[1]}`)); + }); + fs.renameSync(currentName, path.resolve(pluginsFolder, `${newName}.plugin.js`)); + XenoLib.Notifications.success(`[**XenoLib**] \`${pluginName}\` file has been renamed to \`${newName}\``); + if ((!BdApi.Plugins || !BdApi.Plugins.isEnabled || !BdApi.Plugins.enable) && (!global.pluginCookie || !global.pluginModule)) Modals.showAlertModal('Plugin has been renamed', 'Plugin has been renamed, but your client mod has a missing feature, as such, the plugin could not be enabled (if it even was enabled).'); + else { + if (!wasEnabled) return; + setTimeout(() => (BdApi.Plugins && BdApi.Plugins.enable ? BdApi.Plugins.enable(newName) : pluginModule.enablePlugin(newName)), 1000); /* /shrug */ + } + } catch (e) { + Logger.stacktrace('There has been an issue renaming a plugin', e); + } + }; + + const FancyParser = (() => { + const ParsersModule = WebpackModules.getByProps('astParserFor', 'parse'); + try { + const DeepClone = WebpackModules.find(m => m.default && m.default.toString().indexOf('/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(') !== -1 && !m.useVirtualizedAnchor).default; + const ReactParserRules = (WebpackModules.find(m => m.default && m.default.toString().search(/function\(\w\){return \w\({},\w,{link:\(0,\w\.default\)\(\w\)}\)}$/) !== -1) || WebpackModules.find(m => m.default && m.default.toString().search(/function\(\){return \w}$/) !== -1)).default; /* thanks Zere for not fixing the bug ._. */ + const FANCY_PANTS_PARSER_RULES = DeepClone([WebpackModules.getByProps('RULES').RULES, ReactParserRules()]); + const { defaultRules } = WebpackModules.getByProps('defaultParse'); + FANCY_PANTS_PARSER_RULES.image = defaultRules.image; + FANCY_PANTS_PARSER_RULES.link = defaultRules.link; + return ParsersModule.reactParserFor(FANCY_PANTS_PARSER_RULES); + } catch (e) { + Logger.stacktrace('Failed to create special parser', e); + try { + return ParsersModule.parse; + } catch (e) { + Logger.stacktrace('Failed to get even basic parser', e); + return e => e; + } + } + })(); + if (window.Lightcock) return; + const AnchorClasses = WebpackModules.getByProps('anchor', 'anchorUnderlineOnHover') || {}; + const EmbedVideo = (() => { + try { + return WebpackModules.getByProps('EmbedVideo').EmbedVideo; + } catch (e) { + Logger.stacktrace('Failed to get EmbedVideo!', e); + return DiscordConstants.NOOP_NULL; + } + })(); + const VideoComponent = (() => { + try { + const ret = new (WebpackModules.getByDisplayName('MediaPlayer'))({}).render(); + const vc = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf('video-8eMOth') !== -1); + return vc.type; + } catch (e) { + Logger.stacktrace('Failed to get the video component', e); + return DiscordConstants.NOOP_NULL; + } + })(); + const ComponentRenderers = WebpackModules.getByProps('renderVideoComponent') || {}; + const NewModalStack = WebpackModules.getByProps('openModal', 'hasModalOpen'); + /* MY CHANGELOG >:C */ + XenoLib.showChangelog = (title, version, changelog, footer) => { + const ChangelogClasses = DiscordClasses.Changelog; + const items = []; + let isFistType = true; + for (let i = 0; i < changelog.length; i++) { + const item = changelog[i]; + switch (item.type) { + case 'image': + items.push(React.createElement('img', { alt: '', src: item.src, width: item.width || 451, height: item.height || 254 })); + continue; + case 'video': + items.push(React.createElement(VideoComponent, { src: item.src, poster: item.thumbnail, width: item.width || 451, height: item.height || 254, loop: !0, muted: !0, autoPlay: !0, className: ChangelogClasses.video })); + continue; + case 'youtube': + items.push(React.createElement(EmbedVideo, { className: ChangelogClasses.video, allowFullScreen: !1, href: `https://youtu.be/${item.youtube_id}`, thumbnail: { url: `https://i.ytimg.com/vi/${item.youtube_id}/maxresdefault.jpg`, width: item.width || 451, height: item.height || 254 }, video: { url: `https://www.youtube.com/embed/${item.youtube_id}?vq=large&rel=0&controls=0&showinfo=0`, width: item.width || 451, height: item.height || 254 }, width: item.width || 451, height: item.height || 254, renderVideoComponent: ComponentRenderers.renderVideoComponent || DiscordConstants.NOOP_NULL, renderImageComponent: ComponentRenderers.renderImageComponent || DiscordConstants.NOOP_NULL, renderLinkComponent: ComponentRenderers.renderMaskedLinkComponent || DiscordConstants.NOOP_NULL })); + continue; + case 'description': + items.push(React.createElement('p', {}, FancyParser(item.content))); + continue; + default: + const logType = ChangelogClasses[item.type] || ChangelogClasses.added; + items.push(React.createElement('h1', { className: XenoLib.joinClassNames(logType.value, { [ChangelogClasses.marginTop.value]: item.marginTop || isFistType }) }, item.title)); + items.push( + React.createElement( + 'ul', + { className: 'XL-chl-p' }, + item.items.map(e => + React.createElement( + 'li', + {}, + React.createElement( + 'p', + {}, + Array.isArray(e) + ? e.map(e => + Array.isArray(e) + ? React.createElement( + 'ul', + {}, + e.map(e => React.createElement('li', {}, FancyParser(e))) + ) + : FancyParser(e) + ) + : FancyParser(e) + ) + ) + ) + ) + ); + isFistType = false; + } + } + const renderFooter = () => ['Need support? ', React.createElement('a', { className: XenoLib.joinClassNames(AnchorClasses.anchor, AnchorClasses.anchorUnderlineOnHover), onClick: () => (LayerManager.popLayer(), ModalStack.pop(), InviteActions.acceptInviteAndTransitionToInviteChannel('NYvWdN5')) }, 'Join my support server'), '! Or consider donating via ', React.createElement('a', { className: XenoLib.joinClassNames(AnchorClasses.anchor, AnchorClasses.anchorUnderlineOnHover), onClick: () => window.open('https://paypal.me/lighty13') }, 'Paypal'), ', ', React.createElement('a', { className: XenoLib.joinClassNames(AnchorClasses.anchor, AnchorClasses.anchorUnderlineOnHover), onClick: () => window.open('https://ko-fi.com/lighty_') }, 'Ko-fi'), ', ', React.createElement('a', { className: XenoLib.joinClassNames(AnchorClasses.anchor, AnchorClasses.anchorUnderlineOnHover), onClick: () => window.open('https://www.patreon.com/lightyp') }, 'Patreon'), '!']; + NewModalStack.openModal(props => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'Changelog', onError: () => props.onClose() }, React.createElement(ChangelogModal, { className: ChangelogClasses.container, selectable: true, onScroll: _ => _, onClose: _ => _, renderHeader: () => React.createElement(FlexChild.Child, { grow: 1, shrink: 1 }, React.createElement(Titles.default, { tag: Titles.Tags.H4 }, title), React.createElement(TextElement, { size: TextElement.Sizes.SIZE_12, className: ChangelogClasses.date }, `Version ${version}`)), renderFooter: () => React.createElement(FlexChild.Child, { gro: 1, shrink: 1 }, React.createElement(TextElement, { size: TextElement.Sizes.SIZE_12 }, footer ? (typeof footer === 'string' ? FancyParser(footer) : footer) : renderFooter())), children: items, ...props }))); + }; + + /* https://github.com/react-spring/zustand + * MIT License + * + * Copyright (c) 2019 Paul Henschel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + XenoLib.zustand = createState => { + var state; + var listeners = new Set(); + const setState = partial => { + var partialState = typeof partial === 'function' ? partial(state) : partial; + if (partialState !== state) { + state = Object.assign({}, state, partialState); + listeners.forEach(function (listener) { + return listener(); + }); + } + }; + const getState = () => state; + const getSubscriber = (listener, selector, equalityFn) => { + if (selector === void 0) selector = getState; + if (equalityFn === void 0) equalityFn = Object.is; + return { currentSlice: selector(state), equalityFn: equalityFn, errored: false, listener: listener, selector: selector, unsubscribe: function unsubscribe() { } }; + }; + var subscribe = function subscribe(subscriber) { + function listener() { + // Selector or equality function could throw but we don't want to stop + // the listener from being called. + // https://github.com/react-spring/zustand/pull/37 + try { + var newStateSlice = subscriber.selector(state); + if (!subscriber.equalityFn(subscriber.currentSlice, newStateSlice)) subscriber.listener((subscriber.currentSlice = newStateSlice)); + } catch (error) { + subscriber.errored = true; + subscriber.listener(null, error); + } + } + + listeners.add(listener); + return () => listeners.delete(listener); + }; + const apiSubscribe = (listener, selector, equalityFn) => subscribe(getSubscriber(listener, selector, equalityFn)); + const destroy = () => listeners.clear(); + const useStore = (selector, equalityFn) => { + if (selector === void 0) selector = getState; + if (equalityFn === void 0) equalityFn = Object.is; + var forceUpdate = React.useReducer(c => c + 1, 0)[1]; + var subscriberRef = React.useRef(); + if (!subscriberRef.current) { + subscriberRef.current = getSubscriber(forceUpdate, selector, equalityFn); + subscriberRef.current.unsubscribe = subscribe(subscriberRef.current); + } + var subscriber = subscriberRef.current; + var newStateSlice; + var hasNewStateSlice = false; // The selector or equalityFn need to be called during the render phase if + // they change. We also want legitimate errors to be visible so we re-run + // them if they errored in the subscriber. + if (subscriber.selector !== selector || subscriber.equalityFn !== equalityFn || subscriber.errored) { + // Using local variables to avoid mutations in the render phase. + newStateSlice = selector(state); + hasNewStateSlice = !equalityFn(subscriber.currentSlice, newStateSlice); + } // Syncing changes in useEffect. + React.useLayoutEffect(function () { + if (hasNewStateSlice) subscriber.currentSlice = newStateSlice; + subscriber.selector = selector; + subscriber.equalityFn = equalityFn; + subscriber.errored = false; + }); + React.useLayoutEffect(() => subscriber.unsubscribe, []); + return hasNewStateSlice ? newStateSlice : subscriber.currentSlice; + }; + const api = { setState: setState, getState: getState, subscribe: apiSubscribe, destroy: destroy }; + state = createState(setState, getState, api); + return [useStore, api]; + }; + + /* NOTIFICATIONS START */ + let UPDATEKEY = {}; + try { + const DeepEqualityCheck = (content1, content2) => { + if (typeof content1 !== typeof content2) return false; + const isCNT1HTML = content1 instanceof HTMLElement; + const isCNT2HTML = content2 instanceof HTMLElement; + if (isCNT1HTML !== isCNT2HTML) return false; + else if (isCNT1HTML) return content1.isEqualNode(content2); + if (content1 !== content2) { + if (Array.isArray(content1)) { + if (content1.length !== content2.length) return false; + for (const [index, item] of content1.entries()) { + if (!DeepEqualityCheck(item, content2[index])) return false; + } + } else if (typeof content1 === 'object') { + if (content1.type) { + if (typeof content1.type !== typeof content2.type) return false; + if (content1.type !== content2.type) return false; + } + if (typeof content1.props !== typeof content2.props) return false; + if (content1.props) { + if (Object.keys(content1.props).length !== Object.keys(content2.props).length) return false; + for (const prop in content1.props) { + if (!DeepEqualityCheck(content1.props[prop], content2.props[prop])) return false; + } + } + } else return false; + } + return true; + }; + const [useStore, api] = XenoLib.zustand(e => ({ data: [] })); + const defaultOptions = { + loading: false, + progress: -1, + channelId: undefined, + timeout: 3500, + color: '#2196f3', + onLeave: DiscordConstants.NOOP + }; + const utils = { + success(content, options = {}) { + return this.show(content, Object.assign({ color: '#43b581' }, options)); + }, + info(content, options = {}) { + return this.show(content, Object.assign({ color: '#4a90e2' }, options)); + }, + warning(content, options = {}) { + return this.show(content, Object.assign({ color: '#ffa600' }, options)); + }, + danger(content, options = {}) { + return this.show(content, Object.assign({ color: '#f04747' }, options)); + }, + error(content, options = {}) { + return this.danger(content, options); + }, + /** + * @param {string|HTMLElement|React} content - Content to display. If it's a string, it'll be formatted with markdown, including URL support [like this](https://google.com/) + * @param {object} options + * @param {string} [options.channelId] Channel ID if content is a string which gets formatted, and you want to mention a role for example. + * @param {Number} [options.timeout] Set to 0 to keep it permanently until user closes it, or if you want a progress bar + * @param {Boolean} [options.loading] Makes the bar animate differently instead of fading in and out slowly + * @param {Number} [options.progress] 0-100, -1 sets it to 100%, setting it to 100% closes the notification automatically + * @param {string} [options.color] Bar color + * @param {string} [options.allowDuplicates] By default, notifications that are similar get grouped together, use true to disable that + * @param {function} [options.onLeave] Callback when notification is leaving + * @return {Number} - Notification ID. Store this if you plan on force closing it, changing its content or want to set the progress + */ + show(content, options = {}) { + let id = null; + options = Object.assign(Utilities.deepclone(defaultOptions), options); + api.setState(state => { + if (!options.allowDuplicates) { + const notif = state.data.find(n => DeepEqualityCheck(n.content, content) && n.timeout === options.timeout && !n.leaving); + if (notif) { + id = notif.id; + Dispatcher.dirtyDispatch({ type: 'XL_NOTIFS_DUPLICATE', id: notif.id }); + return state; + } + } + if (state.data.length >= 100) return state; + do { + id = Math.floor(4294967296 * Math.random()); + } while (state.data.findIndex(n => n.id === id) !== -1); + return { data: [].concat(state.data, [{ content, ...options, id }]) }; + }); + return id; + }, + remove(id) { + Dispatcher.dirtyDispatch({ type: 'XL_NOTIFS_REMOVE', id }); + }, + /** + * @param {Number} id Notification ID + * @param {object} options + * @param {string} [options.channelId] Channel ID if content is a string which gets formatted, and you want to mention a role for example. + * @param {Boolean} [options.loading] Makes the bar animate differently instead of fading in and out slowly + * @param {Number} [options.progress] 0-100, -1 sets it to 100%, setting it to 100% closes the notification automatically + * @param {string} [options.color] Bar color + * @param {function} [options.onLeave] Callback when notification is leaving + */ + update(id, options) { + delete options.id; + api.setState(state => { + const idx = state.data.findIndex(n => n.id === id); + if (idx === -1) return state; + state.data[idx] = Object.assign(state.data[idx], options); + return state; + }); + Dispatcher.dirtyDispatch({ type: 'XL_NOTIFS_UPDATE', id, ...options }); + }, + exists(id) { + return api.getState().data.findIndex(e => e.id === id && !e.leaving) !== -1; + } + }; + XenoLib.Notifications = utils; + const ReactSpring = WebpackModules.getByProps('useTransition'); + const BadgesModule = WebpackModules.getByProps('NumberBadge'); + const CloseButton = React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24' }, React.createElement('path', { d: 'M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z', fill: 'currentColor' })); + class Notification extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + closeFast: false /* close button pressed, XL_NOTIFS_REMOVE dispatch */, + offscreen: false /* don't do anything special if offscreen, not timeout */, + counter: 1 /* how many times this notification was shown */, + resetBar: false /* reset bar to 0 in the event counter goes up */, + hovered: false, + leaving: true /* prevent hover events from messing up things */, + loading: props.loading /* loading animation, enable progress */, + progress: props.progress /* -1 means undetermined */, + content: props.content, + contentParsed: this.parseContent(props.content, props.channelId), + color: props.color + }; + this._contentRef = null; + this._ref = null; + this._animationCancel = DiscordConstants.NOOP; + this._oldOffsetHeight = 0; + this._initialProgress = !this.props.timeout ? (this.state.loading && this.state.progress !== -1 ? this.state.progress : 100) : 0; + XenoLib._.bindAll(this, ['closeNow', 'handleDispatch', '_setContentRef']); + this.handleResizeEvent = XenoLib._.throttle(this.handleResizeEvent.bind(this), 100); + this.resizeObserver = new ResizeObserver(this.handleResizeEvent); + this._timeout = props.timeout; + } + componentDidMount() { + this._unsubscribe = api.subscribe(_ => this.checkOffScreen()); + window.addEventListener('resize', this.handleResizeEvent); + Dispatcher.subscribe('XL_NOTIFS_DUPLICATE', this.handleDispatch); + Dispatcher.subscribe('XL_NOTIFS_REMOVE', this.handleDispatch); + Dispatcher.subscribe('XL_NOTIFS_UPDATE', this.handleDispatch); + Dispatcher.subscribe('XL_NOTIFS_ANIMATED', this.handleDispatch); + Dispatcher.subscribe('XL_NOTIFS_SETTINGS_UPDATE', this.handleDispatch); + } + componentWillUnmount() { + this._unsubscribe(); + window.window.removeEventListener('resize', this.handleResizeEvent); + Dispatcher.unsubscribe('XL_NOTIFS_DUPLICATE', this.handleDispatch); + Dispatcher.unsubscribe('XL_NOTIFS_REMOVE', this.handleDispatch); + Dispatcher.unsubscribe('XL_NOTIFS_UPDATE', this.handleDispatch); + Dispatcher.unsubscribe('XL_NOTIFS_ANIMATED', this.handleDispatch); + Dispatcher.unsubscribe('XL_NOTIFS_SETTINGS_UPDATE', this.handleDispatch); + this.resizeObserver.disconnect(); + this.resizeObserver = null; /* no mem leaks plz */ + this._ref = null; + this._contentRef = null; + } + handleDispatch(e) { + if (this.state.leaving || this.state.closeFast) return; + if (e.type === 'XL_NOTIFS_SETTINGS_UPDATE') { + if (e.key !== UPDATEKEY) return; + this._animationCancel(); + this.forceUpdate(); + return; + } + if (e.type === 'XL_NOTIFS_ANIMATED') this.checkOffScreen(); + if (e.id !== this.props.id) return; + const { content, channelId, loading, progress, color } = e; + const { content: curContent, channelId: curChannelId, loading: curLoading, progress: curProgress, color: curColor } = this.state; + switch (e.type) { + case 'XL_NOTIFS_REMOVE': + this.closeNow(); + break; + case 'XL_NOTIFS_DUPLICATE': + this._animationCancel(); + this.setState({ counter: this.state.counter + 1, resetBar: !!this.props.timeout, closeFast: false }); + break; + case 'XL_NOTIFS_UPDATE': + if (!this.state.initialAnimDone) { + this.state.content = content || curContent; + this.state.channelId = channelId || curChannelId; + this.state.contentParsed = this.parseContent(content || curContent, channelId || curChannelId); + if (typeof loading !== 'undefined') this.state.loading = loading; + if (typeof progress !== 'undefined') this.state.progress = progress; + this.state.color = color || curColor; + return; + } + this._animationCancel(); + this.setState({ + content: content || curContent, + channelId: channelId || curChannelId, + contentParsed: this.parseContent(content || curContent, channelId || curChannelId), + loading: typeof loading !== 'undefined' ? loading : curLoading, + progress: typeof progress !== 'undefined' ? progress : curProgress, + color: color || curColor + }); + break; + } + } + parseContent(content, channelId) { + if (typeof content === 'string') return FancyParser(content, true, { channelId }); + else if (content instanceof Element) return ReactTools.createWrappedElement(content); + else return content; + } + checkOffScreen() { + if (this.state.leaving || !this._contentRef) return; + const bcr = this._contentRef.getBoundingClientRect(); + if (Math.floor(bcr.bottom) - 1 > Structs.Screen.height || Math.ceil(bcr.top) + 1 < 0) { + if (!this.state.offscreen) { + this._animationCancel(); + this.setState({ offscreen: true }); + } + } else if (this.state.offscreen) { + this._animationCancel(); + this.setState({ offscreen: false }); + } + } + closeNow() { + if (this.state.closeFast) return; + this.resizeObserver.disconnect(); + this._animationCancel(); + api.setState(state => { + const dt = state.data.find(m => m.id === this.props.id); + if (dt) dt.leaving = true; + return { data: state.data }; + }); + this.setState({ closeFast: true }); + } + handleResizeEvent() { + if (this._oldOffsetHeight !== this._contentRef.offsetHeight) { + this._animationCancel(); + this.forceUpdate(); + } + } + _setContentRef(ref) { + if (!ref) return; + this._contentRef = ref; + this.resizeObserver.observe(ref); + } + render() { + const config = { duration: 200 }; + if (this._contentRef) this._oldOffsetHeight = this._contentRef.offsetHeight; + return React.createElement( + ReactSpring.Spring, + { + native: true, + from: { opacity: 0, height: 0, progress: this._initialProgress, loadbrightness: 1 }, + to: async (next, cancel) => { + this.state.leaving = false; + this._animationCancel = cancel; + if (this.state.offscreen) { + if (this.state.closeFast) { + this.state.leaving = true; + await next({ opacity: 0, height: 0 }); + api.setState(state => ({ data: state.data.filter(n => n.id !== this.props.id) })); + return; + } + await next({ opacity: 1, height: this._contentRef.offsetHeight, loadbrightness: 1 }); + if (this.props.timeout) { + await next({ progress: 0 }); + } else { + if (this.state.loading && this.state.progress !== -1) { + await next({ progress: 0 }); + } else { + await next({ progress: 100 }); + } + } + return; + } + const isSettingHeight = this._ref.offsetHeight !== this._contentRef.offsetHeight; + await next({ opacity: 1, height: this._contentRef.offsetHeight }); + if (isSettingHeight) Dispatcher.dirtyDispatch({ type: 'XL_NOTIFS_ANIMATED' }); + this.state.initialAnimDone = true; + if (this.state.resetBar || (this.state.hovered && LibrarySettings.notifications.timeoutReset)) { + await next({ progress: 0 }); /* shit gets reset */ + this.state.resetBar = false; + } + + if (!this.props.timeout && !this.state.closeFast) { + if (!this.state.loading) { + await next({ progress: 100 }); + } else { + await next({ loadbrightness: 1 }); + if (this.state.progress === -1) await next({ progress: 100 }); + else await next({ progress: this.state.progress }); + } + if (this.state.progress < 100 || !this.state.loading) return; + } + if (this.state.hovered && !this.state.closeFast) return; + if (!this.state.closeFast && !LibrarySettings.notifications.timeoutReset) this._startProgressing = Date.now(); + await next({ progress: 100 }); + if (this.state.hovered && !this.state.closeFast) return; /* race condition: notif is hovered, but it continues and closes! */ + this.state.leaving = true; + if (!this.state.closeFast) { + api.setState(state => { + const dt = state.data.find(m => m.id === this.props.id); + if (dt) dt.leaving = true; + return { data: state.data }; + }); + } + this.props.onLeave(); + await next({ opacity: 0, height: 0 }); + api.setState(state => ({ data: state.data.filter(n => n.id !== this.props.id) })); + }, + config: key => { + if (key === 'progress') { + let duration = this._timeout; + if (this.state.closeFast || !this.props.timeout || this.state.resetBar || this.state.hovered) duration = 150; + if (this.state.offscreen) duration = 0; /* don't animate at all */ + return { duration }; + } + if (key === 'loadbrightness') return { duration: 750 }; + return config; + } + }, + e => { + return React.createElement( + ReactSpring.animated.div, + { + style: { + height: e.height, + opacity: e.opacity + }, + className: 'xenoLib-notification', + ref: e => e && (this._ref = e) + }, + React.createElement( + 'div', + { + className: 'xenoLib-notification-content-wrapper', + ref: this._setContentRef, + onMouseEnter: e => { + if (this.state.leaving || !this.props.timeout || this.state.closeFast) return; + this._animationCancel(); + if (this._startProgressing) { + this._timeout -= Date.now() - this._startProgressing; + } + this.state.hovered = true; + this.forceUpdate(); + }, + onMouseLeave: e => { + if (this.state.leaving || !this.props.timeout || this.state.closeFast) return; + this._animationCancel(); + this.setState({ hovered: false }); + }, + style: { + '--grad-one': this.state.color, + '--grad-two': ColorConverter.lightenColor(this.state.color, 20), + '--bar-color': ColorConverter.darkenColor(this.state.color, 30) + }, + onClick: e => { + if (!this.props.onClick) return; + if (e.target && e.target.getAttribute('role') === 'button') return; + this.props.onClick(); + this.closeNow(); + }, + onContextMenu: e => { + if (!this.props.onContext) return; + this.props.onContext(); + this.closeNow(); + } + }, + React.createElement( + 'div', + { + className: 'xenoLib-notification-content', + style: { + backdropFilter: LibrarySettings.notifications.backdrop ? 'blur(5px)' : undefined, + background: ColorConverter.int2rgba(ColorConverter.hex2int(LibrarySettings.notifications.backdropColor), LibrarySettings.notifications.backdrop ? 0.3 : 1.0), + border: LibrarySettings.notifications.backdrop ? 'none' : undefined + }, + ref: e => { + if (!LibrarySettings.notifications.backdrop || !e) return; + e.style.setProperty('backdrop-filter', e.style.backdropFilter, 'important'); + e.style.setProperty('background', e.style.background, 'important'); + e.style.setProperty('border', e.style.border, 'important'); + } + }, + React.createElement(ReactSpring.animated.div, { + className: XenoLib.joinClassNames('xenoLib-notification-loadbar', { 'xenoLib-notification-loadbar-striped': !this.props.timeout && this.state.loading, 'xenoLib-notification-loadbar-user': !this.props.timeout && !this.state.loading }), + style: { right: e.progress.to(e => 100 - e + '%'), filter: e.loadbrightness.to(e => `brightness(${e * 100}%)`) } + }), + React.createElement( + XenoLib.ReactComponents.Button, + { + look: XenoLib.ReactComponents.Button.Looks.BLANK, + size: XenoLib.ReactComponents.Button.Sizes.NONE, + onClick: e => { + e.preventDefault(); + e.stopPropagation(); + this.closeNow(); + }, + onContextMenu: e => { + e.preventDefault(); + e.stopPropagation(); + const state = api.getState(); + state.data.forEach(notif => utils.remove(notif.id)); + }, + className: 'xenoLib-notification-close' + }, + CloseButton + ), + this.state.counter > 1 && BadgesModule.NumberBadge({ count: this.state.counter, className: 'xenLib-notification-counter', color: '#2196f3' }), + this.state.contentParsed + ) + ) + ); + } + ); + } + } + function NotificationsWrapper(e) { + const notifications = useStore(e => { + return e.data; + }); + return notifications.map(item => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: `Notification ${item.id}`, onError: () => api.setState(state => ({ data: state.data.filter(n => n.id !== item.id) })), key: item.id.toString() }, React.createElement(Notification, { ...item, leaving: false }))).reverse(); + } + NotificationsWrapper.displayName = 'XenoLibNotifications'; + const DOMElement = document.createElement('div'); + DOMElement.className = XenoLib.joinClassNames('xenoLib-notifications', `xenoLib-centering-${LibrarySettings.notifications.position}`); + ReactDOM.render(React.createElement(NotificationsWrapper, {}), DOMElement); + document.querySelector('#app-mount').appendChild(DOMElement); + } catch (e) { + Logger.stacktrace('There has been an error loading the Notifications system, fallback object has been put in place to prevent errors', e); + XenoLib.Notifications = { + success(content, options = {}) { }, + info(content, options = {}) { }, + warning(content, options = {}) { }, + danger(content, options = {}) { }, + error(content, options = {}) { }, + show(content, options = {}) { }, + remove(id) { }, + update(id, options) { } + }; + } + /* NOTIFICATIONS END */ + + global.XenoLib = XenoLib; + + const notifLocations = ['topLeft', 'topMiddle', 'topRight', 'bottomLeft', 'bottomMiddle', 'bottomRight']; + const notifLocationClasses = [`${XenoLib.getClass('selected topLeft')} ${XenoLib.getClass('topLeft option')}`, `topMiddle-xenoLib ${XenoLib.getClass('topLeft option')}`, `${XenoLib.getClass('selected topRight')} ${XenoLib.getClass('topLeft option')}`, `${XenoLib.getClass('selected bottomLeft')} ${XenoLib.getClass('topLeft option')}`, `bottomMiddle-xenoLib ${XenoLib.getClass('topLeft option')}`, `${XenoLib.getClass('selected bottomRight')} ${XenoLib.getClass('topLeft option')}`]; + const PositionSelectorWrapperClassname = XenoLib.getClass('topLeft wrapper'); + const PositionSelectorSelectedClassname = XenoLib.getClass('topLeft selected'); + const PositionSelectorHiddenInputClassname = XenoLib.getClass('topLeft hiddenInput'); + const FormText = WebpackModules.getByDisplayName('FormText'); + class NotificationPosition extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + position: props.position + }; + } + componentDidMount() { + this._notificationId = XenoLib.Notifications.show('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur lacinia justo eget libero ultrices mollis.', { timeout: 0 }); + } + componentWillUnmount() { + XenoLib.Notifications.remove(this._notificationId); + } + getSelected() { + switch (this.state.position) { + case 'topLeft': + return 'Top Left'; + case 'topMiddle': + return 'Top Middle'; + case 'topRight': + return 'Top Right'; + case 'bottomLeft': + return 'Bottom Left'; + case 'bottomMiddle': + return 'Bottom Middle'; + case 'bottomRight': + return 'Bottom Right'; + default: + return 'Unknown'; + } + } + render() { + return React.createElement( + 'div', + {}, + React.createElement( + 'div', + { + className: PositionSelectorWrapperClassname + }, + notifLocations.map((e, i) => { + return React.createElement( + 'label', + { + className: XenoLib.joinClassNames(notifLocationClasses[i], { [PositionSelectorSelectedClassname]: this.state.position === e }) + }, + React.createElement('input', { + type: 'radio', + name: 'xenolib-notif-position-selector', + value: e, + onChange: () => { + this.props.onChange(e); + this.setState({ position: e }); + }, + className: PositionSelectorHiddenInputClassname + }) + ); + }) + ), + React.createElement( + FormText, + { + type: FormText.Types.DESCRIPTION, + className: DiscordClasses.Margins.marginTop8 + }, + this.getSelected() + ) + ); + } + } + + class NotificationPositionField extends Settings.SettingField { + constructor(name, note, onChange, value) { + super(name, note, onChange, NotificationPosition, { + position: value, + onChange: reactElement => position => { + this.onChange(position); + } + }); + } + } + + const _radioGroup = WebpackModules.getByDisplayName('RadioGroup'); + class RadioGroupWrapper extends React.PureComponent { + render() { + return React.createElement(_radioGroup, this.props); + } + } + + class RadioGroup extends Settings.SettingField { + constructor(name, note, defaultValue, values, onChange, options = {}) { + super(name, note, onChange, RadioGroupWrapper, { + noteOnTop: true, + disabled: !!options.disabled, + options: values, + onChange: reactElement => option => { + reactElement.props.value = option.value; + reactElement.forceUpdate(); + this.onChange(option.value); + }, + value: defaultValue + }); + } + } + + const _switchItem = WebpackModules.getByDisplayName('SwitchItem'); + class SwitchItemWrapper extends React.PureComponent { + render() { + return React.createElement(_switchItem, this.props); + } + } + + class Switch extends Settings.SettingField { + constructor(name, note, isChecked, onChange, options = {}) { + super(name, note, onChange); + this.disabled = !!options.disabled; + this.value = !!isChecked; + } + + onAdded() { + const reactElement = ReactDOM.render(React.createElement(SwitchItemWrapper, { + children: this.name, + note: this.note, + disabled: this.disabled, + hideBorder: false, + value: this.value, + onChange: (e) => { + reactElement.props.value = e; + reactElement.forceUpdate(); + this.onChange(e); + } + }), this.getElement()); + } + } + + XenoLib.buildSetting = function buildSetting(data) { + const { name, note, type, value, onChange, id } = data; + let setting = null; + if (type == "color") setting = new XenoLib.Settings.ColorPicker(name, note, value, onChange, { disabled: data.disabled, defaultColor: value }); + else if (type == "dropdown") setting = new Settings.Dropdown(name, note, value, data.options, onChange); + else if (type == "file") setting = new Settings.FilePicker(name, note, onChange); + else if (type == "keybind") setting = new Settings.Keybind(name, note, value, onChange); + else if (type == "radio") setting = new RadioGroup(name, note, value, data.options, onChange, { disabled: data.disabled }); + else if (type == "slider") setting = new Settings.Slider(name, note, data.min, data.max, value, onChange, data); + else if (type == "switch") setting = new Switch(name, note, value, onChange, { disabled: data.disabled }); + else if (type == "textbox") setting = new Settings.Textbox(name, note, value, onChange, { placeholder: data.placeholder || "" }); + if (id) setting.id = id; + return setting; + } + + return class CXenoLib extends Plugin { + constructor() { + super(); + this.settings = LibrarySettings; + XenoLib.changeName(__filename, '1XenoLib'); /* prevent user from changing libs filename */ + try { + WebpackModules.getByProps('openModal', 'hasModalOpen').closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + } + load() { + super.load(); + if (window.Lightcock) XenoLib.Notifications.warning(`[${this.getName()}] Lightcord is an unofficial and unsafe client with stolen code that is falsely advertising that it is safe, Lightcord has allowed the spread of token loggers hidden within plugins redistributed by them, and these plugins are not made to work on it. Your account is very likely compromised by malicious people redistributing other peoples plugins, especially if you didn't download this plugin from [GitHub](https://github.com/1Lighty/BetterDiscordPlugins/), you should change your password immediately. Consider using a trusted client mod like [BandagedBD](https://rauenzi.github.io/BetterDiscordApp/) or [Powercord](https://powercord.dev/) to avoid losing your account.`, { timeout: 0 }), location.reload(); + const iZeresPluginLibrary = BdApi.Plugins && BdApi.Plugins.get('ZeresPluginLibrary'); + if (iZeresPluginLibrary && !PluginUpdater.defaultComparator('1.2.26', iZeresPluginLibrary.getVersion())) { + const testEl = document.querySelector('.app-1q1i1E'); + if (testEl && !ReactTools.getReactInstance(testEl)) { + try { + const { join } = require('path'); + const { readFileSync, writeFileSync } = require('fs'); + const zlibpath = join(__dirname, '0PluginLibrary.plugin.js'); + let contents = readFileSync(zlibpath, 'utf8'); + if (contents.indexOf('__reactFiber') === -1) { + contents = contents + .replace('.find((key) => key.startsWith("__reactInternalInstance"))];', '.find((key) => key.startsWith("__reactInternalInstance") || key.startsWith("__reactFiber"))];') + .replace('.find(k => k.startsWith("__reactInternalInstance"));', '.find(k => k.startsWith("__reactInternalInstance") || k.startsWith("__reactFiber"));'); + writeFileSync(zlibpath, contents); + return; + } + } catch { + } + } + } + if (!BdApi.Plugins) return; /* well shit what now */ + if (!BdApi.isSettingEnabled || !BdApi.disableSetting) return; + const prev = BdApi.isSettingEnabled('fork-ps-2'); + if (prev) BdApi.disableSetting('fork-ps-2'); + const list = BdApi.Plugins.getAll().filter(k => k._XL_PLUGIN); + for (let p = 0; p < list.length; p++) { + try { + BdApi.Plugins.reload(list[p].getName()); + } catch (e) { + try { + Logger.stacktrace(`Failed to reload plugin ${list[p].getName()}`, e); + } catch (e) { + Logger.error(`Failed telling you about failing to reload a plugin`, list[p], e); + } + } + } + if (prev) BdApi.enableSetting('fork-ps-2'); + } + buildSetting(data) { + if (data.type === 'position') { + const setting = new NotificationPositionField(data.name, data.note, data.onChange, data.value); + if (data.id) setting.id = data.id; + return setting; + } else if (data.type === 'color') { + const setting = new XenoLib.Settings.ColorPicker(data.name, data.note, data.value, data.onChange, data.options); + if (data.id) setting.id = data.id; + return setting; + } + return XenoLib.buildSetting(data); + } + getSettingsPanel() { + return this.buildSettingsPanel() + .append(new XenoLib.Settings.PluginFooter(() => this.showChangelog())) + .getElement(); + } + saveSettings(category, setting, value) { + this.settings[category][setting] = value; + LibrarySettings[category][setting] = value; + PluginUtilities.saveSettings(this.name, LibrarySettings); + if (category === 'notifications') { + if (setting === 'position') { + const DOMElement = document.querySelector('.xenoLib-notifications'); + if (DOMElement) { + DOMElement.className = XenoLib.joinClassNames('xenoLib-notifications', `xenoLib-centering-${LibrarySettings.notifications.position}`); + Dispatcher.dirtyDispatch({ type: 'XL_NOTIFS_ANIMATED' }); + } + } else if (setting === 'backdrop' || setting === 'backdropColor') { + Dispatcher.wait(() => Dispatcher.dispatch({ type: 'XL_NOTIFS_SETTINGS_UPDATE', key: UPDATEKEY }), (UPDATEKEY = {})); + } + } else if (category === 'addons') { + if (setting === 'extra') { + if (value && !patchAddonCardAnyway.patched) patchAddonCardAnyway(true); + XenoLib.Notifications.warning('Reopen plugins section for immediate effect'); + } + } + } + showChangelog(footer) { + XenoLib.showChangelog(`${this.name} has been updated!`, this.version, this._config.changelog); + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + }; + }; + + /* Finalize */ + + let ZeresPluginLibraryOutdated = false; + try { + if (global.BdApi && 'function' == typeof BdApi.getPlugin) { + const a = (c, a) => ((c = c.split('.').map(b => parseInt(b))), (a = a.split('.').map(b => parseInt(b))), !!(a[0] > c[0])) || !!(a[0] == c[0] && a[1] > c[1]) || !!(a[0] == c[0] && a[1] == c[1] && a[2] > c[2]), + b = BdApi.getPlugin('ZeresPluginLibrary'); + ((b, c) => b && b._config && b._config.info && b._config.info.version && a(b._config.info.version, c))(b, '1.2.26') && (ZeresPluginLibraryOutdated = !0); + } + } catch (e) { + console.error('Error checking if ZeresPluginLibrary is out of date', e); + } + + return !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated + ? class { + constructor() { + this._config = config; + this.start = this.load = this.handleMissingLib; + } + getName() { + return this.name.replace(/\s+/g, ''); + } + getAuthor() { + return this.author; + } + getVersion() { + return this.version; + } + getDescription() { + return this.description + ' You are missing ZeresPluginLibrary for this plugin, please enable the plugin and click Download Now.'; + } + start() { } + stop() { } + handleMissingLib() { + const a = BdApi.findModuleByProps('openModal', 'hasModalOpen'); + if (a && a.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const b = !global.ZeresPluginLibrary, + c = ZeresPluginLibraryOutdated ? 'Outdated Library' : 'Missing Library', + d = `The Library ZeresPluginLibrary required for ${this.name} is ${ZeresPluginLibraryOutdated ? 'outdated' : 'missing'}.`, + e = BdApi.findModuleByDisplayName('Text'), + f = BdApi.findModuleByDisplayName('ConfirmModal'), + g = () => BdApi.alert(c, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, d), `Due to a slight mishap however, you'll have to download the libraries yourself. This is not intentional, something went wrong, errors are in console.`, b || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null)); + if (!a || !f || !e) return console.error(`Missing components:${(a ? '' : ' ModalStack') + (f ? '' : ' ConfirmationModalComponent') + (e ? '' : 'TextElement')}`), g(); + class h extends BdApi.React.PureComponent { + constructor(a) { + super(a), (this.state = { hasError: !1 }); + } + componentDidCatch(a) { + console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), 'function' == typeof this.props.onError && this.props.onError(a); + } + render() { + return this.state.hasError ? null : this.props.children; + } + } + let i = !1, + j = !1; + const k = a.openModal( + b => { + if (j) return null; + try { + return BdApi.React.createElement( + h, + { + label: 'missing dependency modal', + onError: () => { + a.closeModal(k), g(); + } + }, + BdApi.React.createElement( + f, + Object.assign( + { + header: c, + children: BdApi.React.createElement(e, { size: e.Sizes.SIZE_16, children: [`${d} Please click Download Now to download it.`] }), + red: !1, + confirmText: 'Download Now', + cancelText: 'Cancel', + onCancel: b.onClose, + onConfirm: () => { + if (i) return; + i = !0; + const b = require('request'), + c = require('fs'), + d = require('path'); + b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, e, f) => { + try { + if (b || 200 !== e.statusCode) return a.closeModal(k), g(); + c.writeFile(d.join(BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, '0PluginLibrary.plugin.js'), f, () => { }); + } catch (b) { + console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(k), g(); + } + }); + } + }, + b, + { onClose: () => { } } + ) + ) + ); + } catch (b) { + return console.error('There has been an error constructing the modal', b), (j = !0), a.closeModal(k), g(), null; + } + }, + { modalKey: `${this.name}_DEP_MODAL` } + ); + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config)); + })(); + + /*@end@*/ \ No newline at end of file diff --git a/BetterImageViewer.plugin.js b/BetterImageViewer.plugin.js new file mode 100644 index 0000000..1d2f557 --- /dev/null +++ b/BetterImageViewer.plugin.js @@ -0,0 +1,2222 @@ +//META{"name":"BetterImageViewer","source":"https://gitdab.com/hana/lightcock/src/branch/master/BetterImageViewer.plugin.js","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=BetterImageViewer","authorId":"287977955240706060","invite":"9jDgyVjTVX","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.\nJust go to settings, plugins and enable me.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!\nJust go to settings, plugins and enable me!', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); + +@else@*/ + +module.exports = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'BetterImageViewer', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: '1Lighty', + twitter_username: '' + } + ], + version: '1.5.0', + description: 'Move between images in the entire channel with arrow keys, image zoom enabled by clicking and holding, scroll wheel to zoom in and out, hold shift to change lens size. Image previews will look sharper no matter what scaling you have, and will take up as much space as possible.', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/BetterImageViewer/BetterImageViewer.plugin.js' + }, + changelog: [ + { + title: 'fixed', + type: 'fixed', + items: ['Fixed plugin settings.', 'Fixed image scaling fix being disabled when disabling zoom.', 'Fixed opening image in inbox while in friends list throwing an error, still can\'t scroll thru the images tho!'] + }, + { + title: 'Added', + type: 'added', + items: ['Added option to rescale images in chat to be sharper if not at 100% zoom in Discord or Windows.\nEnabled by default.', 'Added option to resize chat images to be way larger (this is a BETA feature, it is leftover code that was never utilized, therefore it has BUGS that I am NOT planning to fix so don\'t ping me about it).\nDisabled by default because unmaintained beta code.'] + } + ], + defaultConfig: [ + { + type: 'category', + id: 'ui', + name: 'UI settings', + collapsible: true, + shown: true, + settings: [ + { + name: 'Show image index and number of images left (estimated)', + id: 'imageIndex', + type: 'switch', + value: true + }, + { + name: 'Show navigation buttons', + id: 'navButtons', + type: 'switch', + value: true + }, + { + name: 'Show image filename', + id: 'infoFilename', + type: 'switch', + value: true + }, + { + name: 'Show image resolution', + note: 'Left is downscaled, right is original', + id: 'infoResolution', + type: 'switch', + value: true + }, + { + name: 'Show image size', + note: "Left is downscaled, right is original, ~ means they're the same", + id: 'infoSize', + type: 'switch', + value: true + }, + { + name: 'Always load full resolution image', + note: "You won't notice a difference. You can also force it to load the full resolution image by ctrl + clicking the image preview. the first resolution on bottom right will turn red when it's enabled.", + id: 'loadFull', + type: 'switch', + value: false + } + ] + }, + { + type: 'category', + id: 'behavior', + name: 'Behavior settings', + collapsible: true, + shown: false, + settings: [ + { + name: 'Use search API', + note: "Without this, you'll only be able to view images currently cached in Discord.", + id: 'searchAPI', + type: 'switch', + value: true + }, + { + name: 'Trigger search when hovering over navigation buttons when needed', + id: 'hoverSearch', + type: 'switch', + value: false + }, + { + name: 'DEBUG', + note: 'do not touch', + id: 'debug', + type: 'switch', + value: false + } + ] + }, + { + type: 'category', + id: 'chat', + name: 'Chat image settings', + collapsible: true, + shown: false, + settings: [ + { + name: 'Scale images to be sharper', + id: 'scale', + type: 'switch', + value: true + }, + { + name: 'Resize images to take up more space', + id: 'resize', + type: 'switchbeta', + value: false + } + ] + }, + { + type: 'category', + id: 'zoom', + name: 'Image Zoom settings', + collapsible: true, + shown: false, + settings: [ + { + name: 'Enable image zoom', + id: 'enabled', + type: 'switch', + value: true + }, + { + name: 'Zoom enable mode', + id: 'enableMode', + type: 'radio', + value: 0, + options: [ + { name: 'Click and hold', value: 0 }, + { name: 'Click to toggle', value: 1 }, + { name: 'Scroll to toggle', value: 2 } + ] + }, + { + name: 'Anti-aliasing', + note: 'On low resolution images, pixels become blurry', + id: 'interp', + type: 'switch', + value: true + }, + { + name: 'Round lens', + note: 'why', + id: 'round', + type: 'switch', + value: true + }, + { + name: 'Allow lens to go out of bounds', + note: 'Allows the lens to go beyond the border of the image', + id: 'outOfBounds', + type: 'switch', + value: false + }, + { + name: 'Allow lens to clip out of view', + note: 'Allows the lens to go beyond the window', + id: 'outOfScreen', + type: 'switch', + value: true + }, + { + name: 'Movement smoothing', + note: 'Not recommended to disable. Smooths out movement and zoom', + id: 'smoothing', + type: 'switch', + value: true + } + ] + } + ] + }; + + /* Build */ + const buildPlugin = ([Plugin, Api]) => { + const { Utilities, WebpackModules, DiscordModules, ReactComponents, DiscordAPI, Logger, Patcher, PluginUtilities, PluginUpdater, Structs } = Api; + const { React, ReactDOM, DiscordConstants, Dispatcher, GuildStore, GuildMemberStore, MessageStore, APIModule, NavigationUtils, SelectedChannelStore } = DiscordModules; + + const ChannelStore = WebpackModules.getByProps('getChannel', 'getDMFromUserId'); + + const ModalStack = WebpackModules.getByProps('openModal', 'hasModalOpen'); + + let PluginBrokenFatal = false; + let NoImageZoom = false; + let overlayDOMNode; + + const { ARROW_LEFT, ARROW_RIGHT } = (() => { + try { + const keys = DiscordConstants.KeyboardKeys; + if (!keys) throw 'KeyboardKeys is undefined'; + return keys; + } catch (e) { + Logger.stacktrace('Failed to get KeyboardKeys', e); + return { ARROW_LEFT: 37, ARROW_RIGHT: 39 }; + } + })(); + const Clickable = WebpackModules.getByDisplayName('Clickable'); + const TextElement = WebpackModules.getByDisplayName('Text'); + + const _ImageUtils = WebpackModules.getByProps('getImageSrc'); + const ImageUtils = Object.assign({}, WebpackModules.getByProps('getImageSrc') || {}, WebpackModules.getByProps('getRatio') || {}, { + zoomFit: (e, t) => ImageUtils.fit(e, t, Math.ceil(Math.round(0.86 * window.innerWidth * devicePixelRatio)), Math.ceil(Math.round(0.8 * window.innerHeight * devicePixelRatio))), + getImageSrc: (e, t, n, r = 1, a = null) => { + var o = t; + var i = n; + if (r < 1) { + o = Math.round(t * r); + i = Math.round(n * r); + } + return ImageUtils.getSrcWithWidthAndHeight(e, t, n, o, i, a); + }, + getSizedImageSrc: (e, t, n, r) => _ImageUtils.getSizedImageSrc(e, t, n, r) + }); + + const TrustedStore = WebpackModules.getByProps('isTrustedDomain'); + + const ChannelMessages = (() => { + try { + const _channelMessages = WebpackModules.getByProps('_channelMessages')._channelMessages; + if (!_channelMessages) throw '_channelMessages is undefined'; + return _channelMessages; + } catch (e) { + Logger.stacktrace('Failed to get _channelMessages! Plugin will not work!', e); + PluginBrokenFatal = true; + return {}; + } + })(); + + const getRatio = (width, height, minW = 400, minH = 300) => ImageUtils.getRatio(width, height, minW, minH); + const getPlaceholder = (src, width, height, minW = 400, minH = 300) => ImageUtils.getImageSrc(src, width, height, getRatio(width, height, minW, minH), null); + + function extractImages(message) { + const images = []; + if (Array.isArray(message.attachments)) { + message.attachments.forEach(({ filename, width, height, url: original, proxy_url: src }) => { + if (!DiscordConstants.IMAGE_RE.test(filename) || (width <= 1 && height <= 1)) return; + const max = ImageUtils.zoomFit(width, height); + const placeholder = getPlaceholder(src, width, height, max.width, max.height); + images.push({ width, height, src, original, placeholder }); + }); + } + if (Array.isArray(message.embeds)) { + const appendImage = image => { + const { width, height, url: original, proxyURL: src } = image; + if (images.findIndex(e => e.src === src) !== -1) return; + if (!src || (width <= 1 && height <= 1)) return; + const max = ImageUtils.zoomFit(width, height); + const placeholder = getPlaceholder(src, width, height, max.width, max.height); + images.push({ width, height, original, src, placeholder }); + }; + message.embeds.forEach(({ image, images }) => { + if (!image && (!images || !images.length)) return; + if (images && images.length) for (const image of images) appendImage(image); + if (image) appendImage(image); + }); + } + return images; + } + const ReactSpring = WebpackModules.getByProps('useTransition'); + const zoomConfig = { mass: 1, tension: 1750, friction: 75, clamp: false }; + + class Image extends (() => { + const Image = WebpackModules.getByDisplayName('Image'); + if (Image) return Image; + Logger.error('Failed to get Image!'); + NoImageZoom = true; + return class error { }; + })() { + constructor(props) { + super(props); + this.state = { zooming: false, visible: false, panelWH: props.__BIV_hiddenSettings.panelWH, zoom: 1.5, loaded: false, failedLoad: false }; + XenoLib._.bindAll(this, ['handleMouseDown', 'handleMouseUp', 'handleMouseWheel', 'setRef']); + try { + this._handleMouseMove = this.handleMouseMove.bind(this); + this.handleMouseMove = e => this.state.zooming && this._handleMouseMove(e.clientX, e.clientY); + this._handleSaveLensWHChangeDC = new TimingModule.DelayedCall(1000, this._handleSaveLensWHChange.bind(this)); + this._controller = new ReactSpring.Controller({ panelX: 0, panelY: 0, panelWH: 0, offsetX: 0, offsetY: 0 }); + this._zoomController = new ReactSpring.Controller({ zoom: 1.5 }); + } catch (err) { + Logger.stacktrace(`Failed constructing Image`, err); + XenoLib.Notifications.error(`[**${config.info.name}**] Image zoom has encountered an error and has been temporarily disabled to prevent Discord from crashing. More info in console.`, { timeout: 0 }); + this.__BIV_crash = true; + } + } + componentDidMount() { + if (super.componentDidMount) super.componentDidMount(); + if (this.__BIV_crash) return; + window.addEventListener('mouseup', this.handleMouseUp); + window.addEventListener('mousemove', this.handleMouseMove); + window.addEventListener('mousewheel', this.handleMouseWheel); + this.getRawImage(); + } + componentWillUnmount() { + if (super.componentWillUnmount) super.componentWillUnmount(); + if (this.__BIV_crash) return; + window.removeEventListener('mouseup', this.handleMouseUp); + window.removeEventListener('mousemove', this.handleMouseMove); + window.removeEventListener('mousewheel', this.handleMouseWheel); + this._handleSaveLensWHChangeDC.cancel(); + this._handleSaveLensWHChange(); + this._controller.dispose(); + this._controller = null; + this._zoomController.dispose(); + this._zoomController = null; + } + componentDidUpdate(prevProps, prevState, snapshot) { + if (super.componentDidUpdate) super.componentDidUpdate(prevProps, prevState, snapshot); + if (this.__BIV_crash) return; + if (this.props.src !== prevProps.src) { + this.state.zoom = 1.5; + this.updateZoomController({ zoom: 1.5, immediate: !this.state.zooming }); + if (this.state.zooming) this.setState({ zooming: false }); + this.getRawImage(); + } else if (this.state.zooming !== prevState.zooming && !this.state.loaded && this.state.raw && !this.state.failedLoad && !this.props.__BIV_animated) { + /* only trigger if zoom state changed, raw image not loaded, raw link set, didn't fail and is not a GIFV */ + if (this.state.zooming) { + if (ImageUtils.isImageLoaded(this.state.raw)) this.setState({ loaded: true }); + else { + /* zoom started, try to load raw */ + this._loadCancel = ImageUtils.loadImage(this.state.raw, failed => { + this._loadCancel = null; + /* load failed, either the URL is invalid, host is dead or it's e621/e926, which is a special case + * do note, the special cases are not handled if SaveToRedux is not present */ + if ((this.state.failedLoad = failed)) return this.updateRawImage(true, true); + this.setState({ loaded: true }); + }); + } + } else { + if (typeof this._loadCancel === 'function') { + this._loadCancel(); + this._loadCancel = null; + } + } + } + if (!this.props.__BIV_settings.enabled) return; + if (!this._ref) return Logger.warn('this._ref is null!'); + this._bcr = this._ref.getBoundingClientRect(); + } + updateController(props, noStart = false) { + this._controller.update({ ...props, immediate: !this.props.__BIV_settings.smoothing || props.immediate, config: zoomConfig }); + if (!noStart) this._controller.start(); + } + updateZoomController(props) { + this._zoomController.update({ ...props, immediate: !this.props.__BIV_settings.smoothing || props.immediate, config: zoomConfig }).start(); + } + _handleSaveLensWHChange() { + Dispatcher.dirtyDispatch({ type: 'BIV_LENS_WH_CHANGE', value: this.state.panelWH }); + } + handleMouseDown(e) { + if (!this.props.__BIV_settings.enabled) return; + if (e.button !== DiscordConstants.MouseButtons.PRIMARY) return; + if (e.ctrlKey) { + Dispatcher.dirtyDispatch({ type: 'BIV_LOAD_FULLRES' }); + return; + } + if (this.state.zooming) return this.setState({ zooming: false }); + else if (this.props.__BIV_settings.enableMode === 2) return; /* scroll to toggle */ + this._handleMouseMove(e.clientX, e.clientY, true); + this.setState({ zooming: true, visible: true }); + e.preventDefault(); + } + handleMouseUp() { + /* click and hold mode */ + if (this.props.__BIV_settings.enableMode !== 0) return; + this.setState({ zooming: false }); + } + handleMouseMove(cx, cy, start) { + this._bcr = this._ref.getBoundingClientRect(); + if (!this.props.__BIV_settings.outOfBounds) { + cx = Math.min(this._bcr.left + this._bcr.width, Math.max(this._bcr.left, cx)); + cy = Math.min(this._bcr.top + this._bcr.height, Math.max(this._bcr.top, cy)); + } + let panelWH = this.state.panelWH; + if (!this.props.__BIV_settings.outOfScreen) { + if (Structs.Screen.height < Structs.Screen.width && panelWH > Structs.Screen.height) panelWH = Structs.Screen.height - 2; + else if (Structs.Screen.height > Structs.Screen.width && panelWH > Structs.Screen.width) panelWH = Structs.Screen.width - 2; + } + const offsetX = cx - this._bcr.left; + const offsetY = cy - this._bcr.top; + let panelX = cx - panelWH / 2; + let panelY = cy - panelWH / 2; + if (!this.props.__BIV_settings.outOfScreen) { + if (panelX < 0) panelX = 0; + else if (panelX + panelWH > Structs.Screen.width) panelX = Structs.Screen.width - (panelWH + 2); + if (panelY < 0) panelY = 0; + else if (panelY + panelWH > Structs.Screen.height) panelY = Structs.Screen.height - (panelWH + 2); + } + this.updateController({ panelX, panelY, offsetX, offsetY, panelWH, immediate: start }); + } + handleMouseWheel(e) { + if (!this.props.__BIV_settings.enabled) return; + /* scroll to toggle mode */ + const scrollToggle = this.props.__BIV_settings.enableMode === 2; + if ((!scrollToggle || (scrollToggle && e.shiftKey)) && !this.state.zooming) return; + if (e.deltaY < 0) { + if (e.shiftKey) { + this.state.panelWH *= 1.1; + if (Structs.Screen.height > Structs.Screen.width && this.state.panelWH > (this.props.__BIV_settings.outOfScreen ? Structs.Screen.height * 2 : Structs.Screen.height)) this.state.panelWH = this.props.__BIV_settings.outOfScreen ? Structs.Screen.height * 2 : Structs.Screen.height - 2; + else if (Structs.Screen.height < Structs.Screen.width && this.state.panelWH > (this.props.__BIV_settings.outOfScreen ? Structs.Screen.width * 2 : Structs.Screen.width)) this.state.panelWH = this.props.__BIV_settings.outOfScreen ? Structs.Screen.width * 2 : Structs.Screen.width - 2; + this.state.panelWH = Math.ceil(this.state.panelWH); + this._handleMouseMove(e.clientX, e.clientY); + this._handleSaveLensWHChangeDC.delay(); + } else { + this.state.zoom = Math.min(this.state.zoom * 1.1, 60); + this.updateZoomController({ zoom: this.state.zoom }); + if (scrollToggle && !this.state.zooming) { + this._handleMouseMove(e.clientX, e.clientY, true); + this.setState({ zooming: true, visible: true }); + } + } + } else if (e.deltaY > 0) { + if (e.shiftKey) { + this.state.panelWH *= 0.9; + if (this.state.panelWH < 75) this.state.panelWH = 75; + this.state.panelWH = Math.ceil(this.state.panelWH); + this._handleMouseMove(e.clientX, e.clientY); + this._handleSaveLensWHChangeDC.delay(); + } else { + const nextZoom = this.state.zoom * 0.9; + this.state.zoom = Math.max(nextZoom, 1); + this.updateZoomController({ zoom: this.state.zoom }); + if (scrollToggle && nextZoom < 1) this.setState({ zooming: false }); + } + } + } + getRawImage(failed) { + if (this.props.__BIV_animated) return; + if (typeof this.__BIV_failNum !== 'number') this.__BIV_failNum = 0; + if (failed) this.__BIV_failNum++; + else this.__BIV_failNum = 0; + const src = this.props.src; + const fullSource = (() => { + const split = src.split('?')[0]; + /* a user experienced some issues due to EXIF data */ + const isJpeg = split.indexOf('//media.discordapp.net/attachments/') !== -1 && split.search(/.jpe?g$/i) !== -1; + const SaveToRedux = BdApi.getPlugin && BdApi.getPlugin('SaveToRedux'); + const needsSize = src.substr(src.indexOf('?')).indexOf('size=') !== -1; + try { + if (SaveToRedux && !PluginUpdater.defaultComparator(SaveToRedux.version, '2.0.12')) return SaveToRedux.formatURL((!isJpeg && this.props.__BIV_original) || '', needsSize, '', '', split, this.__BIV_failNum).url; + } catch (_) { } + return split + (needsSize ? '?size=2048' : ''); + })(); + this.state.failedLoad = false; + this.state.loaded = ImageUtils.isImageLoaded(fullSource); + this.state.raw = fullSource; + } + renderLens(ea, props) { + return React.createElement( + ReactSpring.animated.div, + { + style: { + width: props.panelWH, + height: props.panelWH, + transform: ReactSpring.to([props.panelX, props.panelY], (x, y) => `translate3d(${x}px, ${y}px, 0)`), + opacity: ea.opacity + }, + className: XenoLib.joinClassNames('BIV-zoom-lens', { 'BIV-zoom-lens-round': this.props.__BIV_settings.round }) + }, + React.createElement( + ReactSpring.animated.div, + { + style: { + transform: ReactSpring.to([props.imgContainerLeft, props.imgContainerTop], (x, y) => `translate3d(${-x}px, ${-y}px, 0)`) + } + }, + React.createElement(this.props.__BIV_animated ? ReactSpring.animated.video : ReactSpring.animated.img, { + onError: _ => this.getRawImage(true), + src: this.props.__BIV_animated ? this.props.__BIV_src : this.state.loaded ? this.state.raw : this.props.src, + style: { + transform: props.img.to(({ x, y }) => `translate3d(${x}px, ${y}px, 0)`), + width: props.img.to(({ w }) => w).to(e => e), + height: props.img.to(({ h }) => h).to(e => e) /* even when you animate everything at the same time */, + ...(this.props.__BIV_settings.interp ? {} : { imageRendering: 'pixelated' }) + }, + ...(this.props.__BIV_animated ? { autoPlay: true, muted: true, loop: true } : {}) + }) + ) + ); + } + setRef(e) { + this._ref = e; + } + render() { + const ret = super.render(); + if (this.__BIV_crash) return ret; + for (const prop in ret.props) if (!prop.indexOf('__BIV')) delete ret.props[prop]; + if (!this.props.__BIV_settings.enabled) return ret; + ret.props.onMouseDown = this.handleMouseDown; + ret.ref = this.setRef; + if (this.state.visible) { + ret.props.children.push( + React.createElement( + XenoLib.ReactComponents.ErrorBoundary, + { + label: 'Image zoom', + onError: () => { + XenoLib.Notifications.error(`[**${config.info.name}**] Image zoom has encountered a rendering error and has been temporarily disabled to prevent Discord from crashing. More info in console.`, { timeout: 0 }); + } + }, + ReactDOM.createPortal( + React.createElement( + ReactSpring.Spring, + { + native: true, + from: { opacity: 0 }, + to: { opacity: this.state.zooming ? 1 : 0 }, + config: { duration: 100 }, + onRest: () => { + if (!this.state.zooming) this.setState({ visible: false }); + } + }, + ea => [ + React.createElement(ReactSpring.animated.div, { + style: { + opacity: ea.opacity + }, + className: 'BIV-zoom-backdrop' + }), + this.renderLens(ea, { + imgContainerLeft: this._controller.springs.panelX, + imgContainerTop: this._controller.springs.panelY, + img: ReactSpring.to([this._zoomController.springs.zoom, this._controller.springs.offsetX, this._controller.springs.offsetY], (z, x, y) => { + return { + x: this._bcr.left - ((this._bcr.width * z - this._bcr.width) / (this._bcr.width * z)) * x * z, + y: this._bcr.top - ((this._bcr.height * z - this._bcr.height) / (this._bcr.height * z)) * y * z, + w: z * this.props.width, + h: z * this.props.height + }; + }), + panelX: this._controller.springs.panelX, + panelY: this._controller.springs.panelY, + panelWH: this._controller.springs.panelWH + }) + ] + ), + overlayDOMNode + ) + ) + ); + } + return ret; + } + } + + class LazyImage extends (() => { + const LazyImage = WebpackModules.getByDisplayName('LazyImage'); + if (LazyImage) return LazyImage; + Logger.error('Failed to get LazyImage! Plugin will not work!'); + PluginBrokenFatal = true; + return class error { }; + })() { + constructor(props) { + super(props); + this.renderChildren = this.renderChildren.bind(this); + } + componentDidUpdate(props, prevState, snapshot) { + this._cancellers.forEach(e => e()); + this._cancellers.clear(); + super.componentDidUpdate(props, prevState, snapshot); + if (this.__BIV_updating) return; + this.__BIV_updating = true; + const max = ImageUtils.zoomFit(this.props.width, this.props.height); + const src = getPlaceholder(this.props.src, this.props.width, this.props.height, max.width, max.height); + const isLoaded = ImageUtils.isImageLoaded(src); + if (!isLoaded) { + if (this.state.readyState !== 'LOADING') this.setState({ readyState: 'LOADING' }); + this.loadImage(this.getSrc(this.getRatio(), false), this.handleImageLoad); + } else if (this.state.readyState !== 'READY') { + this.setState({ readyState: 'READY' }); + } + this.__BIV_updating = false; + } + renderChildren(e) { + return React.createElement('img', { + className: e.className || undefined, + alt: e.alt, + src: e.src, + style: e.size, + key: this.props.id /* force React to create a new element for a smooth transition */ + }); + } + render() { + const ret = super.render(); + if (!ret) { + Logger.warn('LazyImage render returned null!', new Error()); /* should not occur */ + return ret; + } + ret.props.__BIV_original = this.props.__BIV_original; + ret.props.children = this.renderChildren; + return ret; + } + } + const MessageRecordUtils = WebpackModules.getByProps('createMessageRecord'); + const Tooltip = WebpackModules.getByDisplayName('Tooltip'); + + const SearchCache = {}; + const OldSearchCache = {}; + const ForwardSearchCache = {}; + const OldForwardSearchCache = {}; + + function stripDeletedMessage(channelId, messageId) { + for (const cache of [SearchCache, OldSearchCache, ForwardSearchCache, OldForwardSearchCache]) { + const chc = cache[channelId]; + if (!Array.isArray(chc)) continue; + const idx = chc.findIndex(e => e.id === messageId); + if (idx === -1) continue; + chc.splice(idx, 1); + } + } + function stripPurgedMessages(channelId, messageIds) { + for (const cache of [SearchCache, OldSearchCache, ForwardSearchCache, OldForwardSearchCache]) { + const chc = cache[channelId]; + if (!Array.isArray(chc)) continue; + for (const messageId of messageIds) { + const idx = chc.findIndex(e => e.id === messageId); + if (idx === -1) continue; + chc.splice(idx, 1); + } + } + } + class ErrorCatcher extends React.PureComponent { + constructor(props) { + super(props); + this.state = { errorLevel: 0, errorTimeout: false }; + } + componentDidCatch(err, inf) { + Logger.err(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`); + this.setState({ errorTimeout: true }); + if (typeof this.props.onError === 'function') this.props.onError(err, this.state.errorLevel); + setImmediate(_ => this.setState({ errorLevel: this.state.errorLevel + 1 })); + } + render() { + if (this.state.errorTimeout) return null; + if (!this.state.errorLevel) return this.props.children; + if (Array.isArray(this.props.fallback) && this.props.fallback[this.state.errorLevel - 1]) return this.props.fallback[this.state.errorLevel - 1]; + return null; + } + } + + const MessageTimestamp = (() => { + try { + const MessageTimestamp = WebpackModules.getByProps('MessageTimestamp').MessageTimestamp; + if (!MessageTimestamp) throw 'MessageTimestamp is undefined'; + return MessageTimestamp; + } catch (e) { + Logger.stacktrace('Failed to get MessageTimestamp! Plugin will not work', e); + PluginBrokenFatal = true; + return () => { }; + } + })(); + + const TimingModule = WebpackModules.getByProps('DelayedCall'); + const APIEncodeModule = WebpackModules.getByProps('stringify', 'parse', 'encode'); + const ImageModal = WebpackModules.getByDisplayName('ImageModal'); + const ImageProps = ['height', 'width', 'original', 'placeholder', 'src']; + const UsernameClassname = XenoLib.getClass('botTag username'); + const ClickableClassname = XenoLib.getClass('username clickable'); + const CozyClassname = XenoLib.getClass('zalgo cozy'); + + /* discord gay */ + const LeftCaretIcon = e => React.createElement('svg', { ...e, name: 'LeftCaret', width: 24, height: 24, viewBox: '0 0 24 24' }, React.createElement('polygon', { points: '18.35 4.35 16 2 6 12 16 22 18.35 19.65 10.717 12', fill: 'currentColor', fillRule: 'nonzero' })); + const RightCaretIcon = e => React.createElement('svg', { ...e, name: 'RightCaret', width: 24, height: 24, viewBox: '0 0 24 24' }, React.createElement('polygon', { points: '8.47 2 6.12 4.35 13.753 12 6.12 19.65 8.47 22 18.47 12', fill: 'currentColor', fillRule: 'nonzero' })); + const WarningTriangleIcon = e => React.createElement('svg', { ...e, name: 'WarningTriangle', width: 16, height: 16, viewBox: '0 0 24 24' }, React.createElement('path', { d: 'M1,21 L23,21 L12,2 L1,21 L1,21 Z M13,18 L11,18 L11,16 L13,16 L13,18 L13,18 Z M13,14 L11,14 L11,10 L13,10 L13,14 L13,14 Z', fill: 'currentColor' })); + const UpdateAvailableIcon = e => React.createElement('svg', { ...e, name: 'UpdateAvailable', width: 16, height: 16, viewBox: '0 0 24 24', className: 'BIV-searching-icon-spin' }, React.createElement('path', { d: 'M5,8 L9,12 L6,12 C6,15.31 8.69,18 12,18 C13.01,18 13.97,17.75 14.8,17.3 L16.26,18.76 C15.03,19.54 13.57,20 12,20 C7.58,20 4,16.42 4,12 L1,12 L5,8 Z M18,12 C18,8.69 15.31,6 12,6 C10.99,6 10.03,6.25 9.2,6.7 L7.74,5.24 C8.97,4.46 10.43,4 12,4 C16.42,4 20,7.58 20,12 L23,12 L19,16 L15,12 L18,12 Z', fill: 'currentColor', fillRule: 'nonzero' })); + const SearchIcon = e => React.createElement('svg', { ...e, name: 'Nova_Search', width: 24, height: 24, viewBox: '0 0 24 24' }, React.createElement('path', { d: 'M21.707 20.293L16.314 14.9C17.403 13.504 18 11.799 18 10C18 7.863 17.167 5.854 15.656 4.344C14.146 2.832 12.137 2 10 2C7.863 2 5.854 2.832 4.344 4.344C2.833 5.854 2 7.863 2 10C2 12.137 2.833 14.146 4.344 15.656C5.854 17.168 7.863 18 10 18C11.799 18 13.504 17.404 14.9 16.314L20.293 21.706L21.707 20.293ZM10 16C8.397 16 6.891 15.376 5.758 14.243C4.624 13.11 4 11.603 4 10C4 8.398 4.624 6.891 5.758 5.758C6.891 4.624 8.397 4 10 4C11.603 4 13.109 4.624 14.242 5.758C15.376 6.891 16 8.398 16 10C16 11.603 15.376 13.11 14.242 14.243C13.109 15.376 11.603 16 10 16Z', fill: 'currentColor' })); + const TimerIcon = e => React.createElement('svg', { ...e, name: 'Timer', width: 16, height: 16, viewBox: '0 0 24 24' }, React.createElement('path', { d: 'M15 1H9v2h6V1zm-4 13h2V8h-2v6zm8.03-6.61l1.42-1.42c-.43-.51-.9-.99-1.41-1.41l-1.42 1.42C16.07 4.74 14.12 4 12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9 9-4.03 9-9c0-2.12-.74-4.07-1.97-5.61zM12 20c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z', fill: 'currentColor', fillRule: 'nonzero' })); + const ClearIcon = e => React.createElement('svg', { ...e, name: 'Clear', width: 18, height: 18, viewBox: '0 0 18 18' }, React.createElement('path', { d: 'M9,2 C12.871,2 16,5.129 16,9 C16,12.871 12.871,16 9,16 C5.129,16 2,12.871 2,9 C2,5.129 5.129,2 9,2 L9,2 Z M11.6925,5.25 L9,7.9425 L6.3075,5.25 L5.25,6.3075 L7.9425,9 L5.25,11.6925 L6.3075,12.75 L9,10.0575 L11.6925,12.75 L12.75,11.6925 L10.0575,9 L12.75,6.3075 L11.6925,5.25 Z', fill: 'currentColor', fillRule: 'nonzero' })); + + const EditorTools = WebpackModules.getByProps('getFirstTextBlock'); + const SearchTools = WebpackModules.getByProps('tokenizeQuery', 'getSearchQueryFromTokens'); + // const SearchResultsWrap = XenoLib.getSingleClass('noResults searchResultsWrap') || 'ERRORCLASS'; + const SearchStore = WebpackModules.getByProps('getCurrentSearchId'); + + const currentChannel = _ => { + const channel = ChannelStore.getChannel(SelectedChannelStore.getChannelId()); + return channel ? Structs.Channel.from(channel) : null; + } + + class RichImageModal extends (() => { + if (ImageModal) return ImageModal; + Logger.error('ImageModal is undefined! Plugin will not work!'); + PluginBrokenFatal = true; + return class error { }; + })() { + constructor(props) { + super(props); + this.state = { + src: props.src, + original: props.original, + width: props.width, + height: props.height, + __BIV_data: props.__BIV_data, + __BIV_index: props.__BIV_index, + requesting: false, + indexing: false, + internalError: false, + rateLimited: false, + localRateLimited: 0, + needsSearch: false, + isNearingEdge: false, + controlsHovered: false, + unknownError: false, + controlsVisible: false, + imageSize: null, + originalImageSize: null, + basicImageInfo: null, + showFullRes: false + }; + XenoLib._.bindAll(this, ['handleMessageCreate', 'handleMessageDelete', 'handlePurge', 'handleKeyDown', 'handlePrevious', 'handleNext', 'handleMouseLeave']); + this.handleMouseEnterLeft = this._handleMouseEnter.bind(this, false); + this.handleMouseEnterRight = this._handleMouseEnter.bind(this, true); + this.handleFastJumpLeft = this._handleFastJump.bind(this, false); + this.handleFastJumpRight = this._handleFastJump.bind(this, true); + if (props.__BIV_index === -1) { + this.state.internalError = true; + return; + } + try { + if (!currentChannel()) { + this.state.internalError = true; + return; + } + const filtered = this.filterMessages(true); + if (!props.__BIV_isSearch && filtered.findIndex(m => m.id === this.state.__BIV_data.messageId) === -1) { + this.state.internalError = true; + return; + } + this._lastSearch = 0; + this._cancellers = new Set(); + this._cachedMessages = [props.__BIV_data.messageId]; + this._preloading = new Set(); + if (!props.__BIV_isSearch) { + if (SearchCache[currentChannel().id]) { + OldSearchCache[currentChannel().id] = [...SearchCache[currentChannel().id]]; + if (SearchCache[currentChannel().id].noBefore) OldSearchCache[currentChannel().id].noBefore = SearchCache[currentChannel().id].noBefore; + if (SearchCache[currentChannel().id]._totalResults) OldSearchCache[currentChannel().id]._totalResults = SearchCache[currentChannel().id]._totalResults; + } + const cache = SearchCache[currentChannel().id]; + if (cache && filtered[0]) { + const idx = cache.findIndex(e => e.id === filtered[0].id); + /* better cache utilization */ + if (idx !== -1) { + this._searchCache = cache.slice(0, idx + 1); + if (cache.noBefore) this._searchCache.noBefore = cache.noBefore; + if (cache._totalResults) this._searchCache._totalResults = cache._totalResults; + SearchCache[currentChannel().id] = this._searchCache; + } + } + if (!this._searchCache) this._searchCache = SearchCache[currentChannel().id] = []; + if (!this._searchCache._totalResults) this._searchCache._totalResults = 0; + if (!ChannelMessages[currentChannel().id].hasMoreBefore) this._searchCache.noBefore = true; + if (ForwardSearchCache[currentChannel().id]) OldForwardSearchCache[currentChannel().id] = [...ForwardSearchCache[currentChannel().id]]; + if (ChannelMessages[currentChannel().id].hasMoreAfter && !ChannelMessages[currentChannel().id]._after._wasAtEdge) { + filtered.reverse(); + const cache = ForwardSearchCache[currentChannel().id]; + if (cache && filtered[0]) { + const idx = cache.findIndex(e => e.id === filtered[0].id); + /* god I hope I did this right */ + if (idx !== -1) { + this._forwardSearchCache = cache.slice(idx); + ForwardSearchCache[currentChannel().id] = this._forwardSearchCache; + } + } + } + if (!this._forwardSearchCache) this._forwardSearchCache = ForwardSearchCache[currentChannel().id] = []; + this._followNew = ChannelMessages[currentChannel().id]._after._wasAtEdge; + this._searchId = DiscordAPI.currentGuild ? DiscordAPI.currentGuild.id : currentChannel().id; + } else { + this._followNew = false; + this._searchCache = []; + this._forwardSearchCache = []; + const images = []; + this._searchId = SearchStore.getCurrentSearchId(); + const searchResults = SearchStore.getResults(this._searchId); + this._includeNonHits = !searchResults.some(m => m.findIndex(e => e.id === props.__BIV_data.messageId && e.isSearchHit) !== -1); + searchResults.forEach(group => { + group.forEach(iMessage => { + if ((!this._includeNonHits && !iMessage.isSearchHit) || images.findIndex(e => e.id === iMessage.id) !== -1) return; + if (!extractImages(iMessage).length) return; + images.push(iMessage); + }); + }); + images.sort((a, b) => a.timestamp.unix() - b.timestamp.unix()); + /* discord search is le broken lol */ + /* if (Utilities.getNestedProp(ZeresPluginLibrary.ReactTools.getOwnerInstance(document.querySelector(`.${SearchResultsWrap}`)), 'state.searchMode') === DiscordConstants.SearchModes.OLDEST) images.reverse(); */ + this._searchCache._totalResults = SearchStore.getTotalResults(this._searchId); + this._searchCache.unshift(...images); + let searchString = EditorTools.getFirstTextBlock(SearchStore.getEditorState(this._searchId)); + let searchquery; + let o; + let s; + for (o = SearchTools.tokenizeQuery(searchString), searchquery = SearchTools.getSearchQueryFromTokens(o), s = 0; s < o.length; s++) { + SearchTools.filterHasAnswer(o[s], o[s + 1]) || (searchString = searchString.substring(0, o[s].start) + searchString.substring(o[s].end)); + } + this._searchProps = searchquery; + } + this._searchType = GuildStore.getGuild(this._searchId) ? DiscordConstants.SearchTypes.GUILD : DiscordConstants.SearchTypes.CHANNEL; + this._imageCounter = {}; + this._oFM = []; + this.calculateImageNumNMax(); + } catch (err) { + Logger.stacktrace('Failed constructing RichImageModal', err); + /* XenoLib.Notifications.error(`[**${config.info.name}**] Serious internal error. More info in console.`) */ + this.state.internalError = -1; + } + } + componentDidMount() { + if (super.componentDidMount) super.componentDidMount(); + if (this.state.internalError) return; + window.addEventListener('keydown', this.handleKeyDown); + Dispatcher.subscribe('MESSAGE_CREATE', this.handleMessageCreate); + Dispatcher.subscribe('MESSAGE_DELETE', this.handleMessageDelete); + Dispatcher.subscribe('MESSAGE_DELETE_BULK', this.handlePurge); + this.handlePreLoad(); + } + componentWillUnmount() { + if (super.componentWillUnmount) super.componentWillUnmount(); + if (this.state.internalError) return; + window.removeEventListener('keydown', this.handleKeyDown); + Dispatcher.unsubscribe('MESSAGE_CREATE', this.handleMessageCreate); + Dispatcher.unsubscribe('MESSAGE_DELETE', this.handleMessageDelete); + Dispatcher.unsubscribe('MESSAGE_DELETE_BULK', this.handlePurge); + this._cancellers.forEach(e => e()); + this._cancellers.clear(); + } + filterMessages(noCache) { + const chan = this.props.__BIV_isSearch ? [] : ChannelMessages[currentChannel().id]; + const arr = [...((!noCache && this._searchCache) || []), ...(!this.props.__BIV_isSearch ? [...chan._before._messages, ...chan._array, ...chan._after._messages] : []), ...((!noCache && this._forwardSearchCache) || [])]; + return arr.filter((m, i) => arr.findIndex(a => a.id === m.id) === i && extractImages(m).length).sort((a, b) => a.timestamp.unix() - b.timestamp.unix()); + } + getMessage(id) { + return MessageStore.getMessage(currentChannel().id, id) || this.filterMessages().find(m => m.id === id); + } + calculateImageNumNMax() { + const filtered = this.filterMessages(); + this._oFM = [...filtered]; + filtered.reverse(); + this._imageCounter = {}; + let imageCount = 1; + filtered.forEach(message => { + const images = extractImages(message); + this._imageCounter[message.id] = imageCount; + imageCount += images.length; + }); + this._maxImages = imageCount - 1; + } + processCache(cache, lastId, reverse) { + const OldChannelCache = cache[currentChannel().id]; + if (OldChannelCache && OldChannelCache.findIndex(m => m.id === lastId) !== -1) { + const idx = OldChannelCache.findIndex(m => m.id === lastId); + const images = reverse ? OldChannelCache.slice(idx) : OldChannelCache.slice(0, idx + 1); + if (images.length > 2) { + images.sort((a, b) => a.timestamp.unix() - b.timestamp.unix()); + if (reverse) { + this._forwardSearchCache.push(...images); + } else { + this._searchCache.unshift(...images); + if (OldChannelCache.noBefore) this._searchCache.noBefore = OldChannelCache.noBefore; + if (OldChannelCache._totalResults) this._searchCache._totalResults = OldChannelCache._totalResults; + } + this.calculateImageNumNMax(); + this.forceUpdate(); + return true; + } + } + } + handleSearch(lastId, reverse) { + if (!this.props.__BIV_settings.behavior.searchAPI) return; + if (!this.props.__BIV_isSearch && reverse && !ChannelMessages[currentChannel().id].hasMoreAfter) return Logger.warn("Illegal operation, attempted to reverse search, but we're on newest image\n", new Error().stack); + this.state.needsSearch = false; + if ((this.state.requesting && !this.state.indexing) || (!reverse && this._searchCache.noBefore) || (reverse && this._followNew)) return; + /* fully utilize both caches */ + if (!this.props.__BIV_isSearch && this.processCache(OldForwardSearchCache, lastId, reverse)) return; + if (!this.props.__BIV_isSearch && this.processCache(OldSearchCache, lastId, reverse)) return; + if (this.state.rateLimited) return; + if (!this.state.indexing && Date.now() - this._lastSearch < 1500) { + if (!this.state.localRateLimited) { + this.state.localRateLimited = this.setState({ + localRateLimited: setTimeout(() => { + this.state.localRateLimited = 0; + this.handleSearch(lastId, reverse); + }, 1500 - (Date.now() - this._lastSearch)) + }); + } + return; + } + this._lastSearch = Date.now(); + const query = Object.assign({}, this.props.__BIV_isSearch ? this._searchProps : { channel_id: currentChannel().id }, { has: 'image', include_nsfw: true, [reverse ? 'min_id' : 'max_id']: lastId }, reverse ? { sort_order: 'asc' } : {}); + APIModule.get({ + url: this._searchType === DiscordConstants.SearchTypes.GUILD ? DiscordConstants.Endpoints.SEARCH_GUILD(this._searchId) : DiscordConstants.Endpoints.SEARCH_CHANNEL(this._searchId), + query: APIEncodeModule.stringify(query) + }) + .then(content => { + if (content.status === 202) { + this.setState({ indexing: true }); + setTimeout(() => this.handleSearch(lastId, reverse), content.body.retry_after || 5e3); + return; + } else if (content.status === 429) { + this.setState({ rateLimited: content.body.retry_after || 5e3 }); + setTimeout(() => { + this.setState({ rateLimited: false }); + this.handleSearch(lastId, reverse); + }, content.body.retry_after || 5e3); + return; + } else if (content.status >= 400) { + throw `Status ${content.status}`; + } + if (content.body.total_results <= 25) { + if (reverse) this._followNew = true; + else this._searchCache.noBefore = true; + } + const filtered = this.filterMessages(); + const images = [reverse ? filtered[filtered.length - 1] : filtered[0]]; + content.body.messages.forEach(group => { + group.forEach(message => { + if ((this.props.__BIV_isSearch && !this._includeNonHits && !message.hit) || images.findIndex(e => e.id === message.id) !== -1) return; + const iMessage = MessageRecordUtils.createMessageRecord(message); + if (!extractImages(iMessage).length) return; + images.push(iMessage); + }); + }); + images.sort((a, b) => a.timestamp.unix() - b.timestamp.unix()); + if (reverse) { + this._forwardSearchCache.push(...images); + } else { + if (this._searchCache.noBefore) this._searchCache._totalResults = 0; + else this._searchCache._totalResults = content.body.total_results - 25; + this._searchCache.unshift(...images); + } + this.calculateImageNumNMax(); + this.setState({ requesting: false, indexing: false }); + }) + .catch(err => (Logger.stacktrace('There has been an issue searching', err), this.setState({ unknownError: true, indexing: false }), setTimeout(() => this.setState({ requesting: false }), 1000))); + this.setState({ requesting: true, unknownError: false }); + } + handleMessageCreate({ optimistic, channelId, message }) { + if (this.props.__BIV_isSearch) return; + if (optimistic || channelId !== currentChannel().id || !extractImages(message).length) return; + if (this._followNew) this._forwardSearchCache.push(MessageRecordUtils.createMessageRecord(message)); + this.calculateImageNumNMax(); + this.forceUpdate(); + } + handleMessageDeletes() { + /* While for people that have loggers, which preserve deleted and purged messages, + this is a non issue as the image stays. However for everyone else, if the image + we are currently displaying is deleted, we will get lost and be unable to tell what + image is before and after the current image. So force the user to go to a non + deleted image, if that fails, force close we don't want to cause bugs like that, + also it might trip internalError if we don't iirc which will hide everything the + plugin adds, that's a big nono. + */ + if (this.handleStart(true, true) && this.handleEnd(true, true)) { + this.props.onClose(); /* we are trapped on an image that does not exist, give up */ + return XenoLib.Notifications.warning('[**BetterImageViewer**] All visible images were deleted, forcefully closed to avoid issues.', { timeout: 0 }); /* tell the user about the sudden closure, ps toasts suck for this */ + } + this.calculateImageNumNMax(); + this.forceUpdate(); + } + handleMessageDelete(e) { + const { channelId, id: messageId } = e; + stripDeletedMessage(channelId, messageId); + if (messageId !== this.state.__BIV_data.messageId) return; + this.handleMessageDeletes(); + } + handlePurge(e) { + const { channelId, ids: messageIds } = e; + stripPurgedMessages(channelId, messageIds); + if (channelId !== currentChannel().id || messageIds.indexOf(this.state.__BIV_data.messageId) === -1) return; + for (const messageId of messageIds) { + if (messageId === this.state.__BIV_data.messageId) continue; + const idx = this._oFM.findIndex(e => e.id === messageId); + if (idx === -1) continue; + this._oFM.splice(idx, 1); + } + this.handleMessageDeletes(); + } + _handleMouseEnter(next) { + this.state.controlsHovered = true; + if (this.props.__BIV_settings.behavior.debug) this.forceUpdate(); + if (!this.state.needsSearch || (this.state.needsSearch === -1 && !next) || !this.props.__BIV_settings.behavior.hoverSearch) return; + const filtered = this.filterMessages(); + this.handleSearch(next ? filtered[filtered.length - 1].id : filtered[0].id, next); + } + handleMouseLeave() { + this.state.controlsHovered = false; + if (this.props.__BIV_settings.behavior.debug) this.forceUpdate(); + } + handlePreLoad(keyboardMode, next, subsidiaryMessageId) { + const filtered = this.filterMessages(); + const targetIdx = filtered.findIndex(m => m.id === (subsidiaryMessageId ? subsidiaryMessageId : this.state.__BIV_data.messageId)); + if (targetIdx === -1) Logger.warn('Unknown message\n', new Error()); + const isNearingEdge = next ? filtered.length - (targetIdx + 1) < 5 : targetIdx + 1 < 5; + this.setState({ isNearingEdge, showFullRes: false }); + if (keyboardMode === -1 || isNearingEdge) { + /* search required, wait for user input if none of these are tripped */ + if (keyboardMode || this.state.controlsHovered) { + if (!next || (next && (this.props.__BIV_isSearch || ChannelMessages[currentChannel().id].hasMoreAfter))) this.handleSearch(next ? filtered[filtered.length - 1].id : filtered[0].id, next); + } else { + this.state.needsSearch = next ? -1 : 1; + } + } + if (targetIdx === -1) { + XenoLib.Notifications.error(`[**${config.info.name}**] Anomaly detected, disabling controls.`, { timeout: 5000 }); + return this.setState({ internalError: true }); + } + const handleMessage = message => { + if (this._cachedMessages.indexOf(message.id) !== -1 || this._preloading.has(message.id)) return; + const data = extractImages(message); + data.forEach(image => { + const max = ImageUtils.zoomFit(image.width, image.height); + const src = getPlaceholder(image.src, image.width, image.height, max.width, max.height); + if (ImageUtils.isImageLoaded(src)) { + this._cachedMessages.push(message.id); + return; + } + const cancel = ImageUtils.loadImage(src, (e, r) => { + if (cancel) this._cancellers.delete(cancel); + this._cachedMessages.push(message.id); + this._preloading.delete(message.id); + }); + if (cancel) this._cancellers.add(cancel); + this._preloading.add(message.id); + }); + }; + filtered.slice(Math.max(targetIdx - 5, 0), Math.max(targetIdx, 0)).forEach(handleMessage); + filtered.slice(Math.min(targetIdx + 1, filtered.length), Math.min(targetIdx + 5, filtered.length)).forEach(handleMessage); + } + handleKeyDown(e) { + if (this.state.internalError) return; + switch (e.which) { + case ARROW_LEFT: + case ARROW_RIGHT: + e.preventDefault(); + this.state.controlsInactive = true; + this.handleChangeImage(e.which === ARROW_RIGHT, true); + } + } + handleEnd(keyboardMode, useInternal) { + const filtered = useInternal ? this._oFM : this.filterMessages(); + const previousMessage = filtered.find((e, i) => filtered[i - 1] && filtered[i - 1].id === this.state.__BIV_data.messageId); + if (!previousMessage) return true; + const newData = { + images: extractImages(previousMessage), + messageId: previousMessage.id + }; + this.setState({ + __BIV_data: newData, + __BIV_index: 0, + ...newData.images[0] + }); + this.requestImageInfo(newData.images[0]); + this.handlePreLoad(keyboardMode, true, previousMessage.id); + } + handleStart(keyboardMode, useInternal) { + const filtered = useInternal ? this._oFM : this.filterMessages(); + const previousMessage = filtered.find((e, i) => filtered[i + 1] && filtered[i + 1].id === this.state.__BIV_data.messageId); + if (!previousMessage) return true; + const newData = { + images: extractImages(previousMessage), + messageId: previousMessage.id + }; + this.setState({ + __BIV_data: newData, + __BIV_index: newData.images.length - 1, + ...newData.images[newData.images.length - 1] + }); + this.requestImageInfo(newData.images[newData.images.length - 1]); + this.handlePreLoad(keyboardMode, false, previousMessage.id); + } + handleChangeImage(next, keyboardMode) { + if (next) { + if (this.state.__BIV_index === this.state.__BIV_data.images.length - 1) { + if (!this.handleEnd(keyboardMode)) return; + } else this.state.__BIV_index++; + } else { + if (!this.state.__BIV_index) { + if (!this.handleStart(keyboardMode)) return; + } else this.state.__BIV_index--; + } + this.handlePreLoad(keyboardMode, next); + this.setState(this.state.__BIV_data.images[this.state.__BIV_index]); + this.requestImageInfo(this.state.__BIV_data.images[this.state.__BIV_index]); + } + handlePrevious() { + this.handleChangeImage(); + } + handleNext() { + this.handleChangeImage(true); + } + _handleFastJump(next) { + const filtered = this.filterMessages(); + const iMessage = next ? filtered[filtered.length - 1] : filtered[0]; + const newData = { + images: extractImages(iMessage), + messageId: iMessage.id + }; + this.setState({ + __BIV_data: newData, + __BIV_index: newData.images.length - 1, + ...newData.images[newData.images.length - 1] + }); + this.requestImageInfo(newData.images[newData.images.length - 1]); + this.handlePreLoad(-1, next); + } + render() { + if (this.state.internalError === -1) throw 'If you see this, something went HORRIBLY wrong!'; + for (const prop of ImageProps) this.props[prop] = this.state[prop]; + const ret = super.render(); + if (this.state.internalError) return ret; + const message = this.state.__BIV_data && this.getMessage(this.state.__BIV_data.messageId); + if ((!message && this.state.__BIV_data)) return ret; + if (!message) { + if (!this.__couldNotFindMessage) XenoLib.Notifications.error(`[**${config.info.name}**] Something went wrong.. Could not find associated message for current image.`, { timeout: 7500 }); + this.__couldNotFindMessage = true; + return ret; + } else this.__couldNotFindMessage = false; + const currentImage = this._imageCounter[this.state.__BIV_data.messageId] + (this.state.__BIV_data.images.length - 1 - this.state.__BIV_index); + ret.props.children[0].type = LazyImage; + ret.props.children[0].props.id = message.id + currentImage; + ret.props.children[0].props.__BIV_original = this.props.original; + const iMember = DiscordAPI.currentGuild && GuildMemberStore.getMember(DiscordAPI.currentGuild.id, message.author.id); + ret.props.children.push( + ReactDOM.createPortal( + [ + this.props.__BIV_settings.ui.navButtons || this.props.__BIV_settings.behavior.debug + ? [ + React.createElement( + Clickable, + { + className: XenoLib.joinClassNames('BIV-left', { 'BIV-disabled': currentImage === this._maxImages && (this._searchCache.noBefore || this.state.rateLimited), 'BIV-inactive': this.state.controlsInactive, 'BIV-hidden': !this.state.controlsVisible }), + onClick: this.handlePrevious, + onContextMenu: this.handleFastJumpLeft, + onMouseEnter: this.handleMouseEnterLeft, + onMouseLeave: this.handleMouseLeave + }, + React.createElement(LeftCaretIcon) + ), + React.createElement( + Clickable, + { + className: XenoLib.joinClassNames('BIV-right', { 'BIV-disabled': currentImage === 1, 'BIV-inactive': this.state.controlsInactive, 'BIV-hidden': !this.state.controlsVisible }), + onClick: this.handleNext, + onContextMenu: this.handleFastJumpRight, + onMouseEnter: this.handleMouseEnterRight, + onMouseLeave: this.handleMouseLeave + }, + React.createElement(RightCaretIcon) + ) + ] + : null, + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-info', { 'BIV-inactive': this.state.controlsInactive, 'BIV-hidden': !this.state.controlsVisible }) + }, + this.props.__BIV_settings.ui.imageIndex || this.props.__BIV_settings.behavior.debug + ? React.createElement( + TextElement, + { + className: 'BIV-text-bold' + }, + 'Image ', + currentImage, + ' of ', + this._maxImages, + this._searchCache._totalResults || this.props.__BIV_settings.behavior.debug + ? React.createElement( + Tooltip, + { + text: `Estimated ${this._maxImages + this._searchCache._totalResults} images in current channel`, + position: 'top' + }, + e => React.createElement('span', e, ' (~', this._maxImages + this._searchCache._totalResults, ')') + ) + : undefined + ) + : null, + React.createElement( + 'div', + { + className: 'BIV-info-wrapper' + }, + React.createElement( + TextElement, + { + className: XenoLib.joinClassNames(CozyClassname, 'BIV-info-wrapper-text') + }, + React.createElement( + 'span', + { + className: XenoLib.joinClassNames(UsernameClassname, ClickableClassname), + onContextMenu: e => { + WebpackModules.getByProps('openUserContextMenu').openUserContextMenu(e, message.author, ChannelStore.getChannel(message.channel_id)); + }, + style: + iMember && iMember.colorString + ? { + color: iMember.colorString + } + : null, + onClick: () => { + this.props.onClose(); + NavigationUtils.transitionTo(`/channels/${(DiscordAPI.currentGuild && DiscordAPI.currentGuild.id) || '@me'}/${message.channel_id}${message.id ? '/' + message.id : ''}`); + } + }, + (iMember && iMember.nick) || message.author.username + ), + React.createElement(MessageTimestamp, { + timestamp: DiscordModules.Moment(message.timestamp) + }), + (this.props.__BIV_settings.behavior.debug || this._searchCache.noBefore) && + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-requesting', TextElement.Colors.ERROR) + }, + React.createElement( + Tooltip, + { + text: 'You have reached the start of the channel' + }, + e => React.createElement(LeftCaretIcon, e) + ) + ), + (this.props.__BIV_settings.behavior.debug || (this.state.isNearingEdge && !this.props.__BIV_settings.behavior.searchAPI)) && + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-requesting', TextElement.Colors.STATUS_YELLOW) + }, + React.createElement( + Tooltip, + { + text: 'You are nearing the edge of available images. If you want more, enable search API.' + }, + e => React.createElement(WarningTriangleIcon, e) + ) + ), + (this.props.__BIV_settings.behavior.debug || (this.state.requesting && !this.state.unknownError)) && + React.createElement( + 'div', + { + className: 'BIV-requesting' + }, + React.createElement( + Tooltip, + { + text: 'Requesting more...' + }, + e => React.createElement(UpdateAvailableIcon, e) + ) + ), + (this.props.__BIV_settings.behavior.debug || this.state.indexing) && + React.createElement( + 'div', + { + className: 'BIV-requesting' + }, + React.createElement( + Tooltip, + { + text: 'Indexing channel...' + }, + e => React.createElement(SearchIcon, e) + ) + ), + this.props.__BIV_settings.behavior.debug || this.state.localRateLimited || this.state.rateLimited + ? React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-requesting', TextElement.Colors.ERROR) + }, + React.createElement( + Tooltip, + { + text: 'You have been rate limited, please wait' + }, + e => React.createElement(TimerIcon, e) + ) + ) + : undefined, + (this.props.__BIV_settings.behavior.debug || this._followNew) && + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-requesting', TextElement.Colors.ERROR) + }, + React.createElement( + Tooltip, + { + text: 'You have reached the end of the channel and are listening for new images' + }, + e => React.createElement(RightCaretIcon, e) + ) + ), + (this.props.__BIV_settings.behavior.debug || this.state.unknownError) && + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-requesting', TextElement.Colors.ERROR) + }, + React.createElement( + Tooltip, + { + text: 'Unknown error occured' + }, + e => React.createElement(ClearIcon, e) + ) + ) + ) + ) + ) + ], + overlayDOMNode + ) + ); + return ret; + } + } + + const BetaClasses = WebpackModules.find(e => e.beta && !e.channel); + + return class BetterImageViewer extends Plugin { + constructor() { + super(); + XenoLib.changeName(__filename, this.name); + this.handleWHChange = this.handleWHChange.bind(this); + this.showChangelog = this.showChangelog.bind(this); + const oOnStart = this.onStart.bind(this); + this._startFailure = message => { + PluginUpdater.checkForUpdate(this.name, this.version, this._config.info.github_raw); + XenoLib.Notifications.error(`[**${this.name}**] ${message} Please update it, press CTRL + R, or ${GuildStore.getGuild(XenoLib.supportServerId) ? 'go to <#639665366380838924>' : '[join my support server](https://discord.gg/NYvWdN5)'} for further assistance.`, { timeout: 0 }); + }; + this.onStart = () => { + try { + oOnStart(); + } catch (e) { + Logger.stacktrace('Failed to start!', e); + this._startFailure('Failed to start!'); + try { + this.onStop(); + } catch (e) { } + } + }; + try { + ModalStack.closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + } + onStart() { + if (!overlayDOMNode) { + overlayDOMNode = document.createElement('div'); + overlayDOMNode.className = 'biv-overlay'; + } + document.querySelector('#app-mount').append(overlayDOMNode); + this.promises = { state: { cancelled: false } }; + if (window.Lightcock) XenoLib.Notifications.warning(`[${this.getName()}] Lightcord is an unofficial and unsafe client with stolen code that is falsely advertising that it is safe, Lightcord has allowed the spread of token loggers hidden within plugins redistributed by them, and these plugins are not made to work on it. Your account is very likely compromised by malicious people redistributing other peoples plugins, especially if you didn't download this plugin from [GitHub](https://github.com/1Lighty/BetterDiscordPlugins/edit/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js), you should change your password immediately. Consider using a trusted client mod like [BandagedBD](https://rauenzi.github.io/BetterDiscordApp/) or [Powercord](https://powercord.dev/) to avoid losing your account.`, { timeout: 0 }); + if (PluginBrokenFatal) return this._startFailure('Plugin is in a broken state.'); + if (NoImageZoom) this._startFailure('Image zoom is broken.'); + if (this.settings.zoom.enabled && !NoImageZoom && BdApi.getPlugin('ImageZoom') && BdApi.Plugins.isEnabled('ImageZoom')) XenoLib.Notifications.warning(`[**${this.name}**] Using **ImageZoom** while having the zoom function in **${this.name}** enabled is unsupported! Please disable one or the other.`, { timeout: 15000 }); + if (BdApi.getPlugin('Better Image Popups') && BdApi.Plugins.isEnabled('Better Image Popups')) XenoLib.Notifications.warning(`[**${this.name}**] Using **Better Image Popups** with **${this.name}** is completely unsupported and will cause issues. **${this.name}** fully supersedes it in terms of features as well, please either disable **Better Image Popups** or delete it to avoid issues.`, { timeout: 0 }); + if (this.settings.zoom.enabled && BdApi.getPlugin('ImageGallery') && BdApi.Plugins.isEnabled('ImageGallery')) XenoLib.Notifications.warning(`[**${this.name}**] Using **ImageGallery** with **${this.name}** is completely unsupported and will cause issues, mainly, zoom breaks. **${this.name}** fully supersedes it in terms of features as well, please either disable **ImageGallery** or delete it to avoid issues.`, { timeout: 0 }); + this.hiddenSettings = XenoLib.loadData(this.name, 'hidden', { panelWH: 500 }); + this.patchAll(); + Dispatcher.subscribe('MESSAGE_DELETE', this.handleMessageDelete); + Dispatcher.subscribe('MESSAGE_DELETE_BULK', this.handlePurge); + Dispatcher.subscribe('BIV_LENS_WH_CHANGE', this.handleWHChange); + const o = Error.captureStackTrace; + const ol = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + try { + const check1 = a => a[0] === 'L' && a[3] === 'h' && a[7] === 'r'; + const check2 = a => a.length === 13 && a[0] === 'B' && a[7] === 'i' && a[12] === 'd'; + const mod = WebpackModules.find(e => Object.keys(e).findIndex(check1) !== -1) || {}; + (Utilities.getNestedProp(mod, `${Object.keys(mod).find(check1)}.${Object.keys(Utilities.getNestedProp(mod, Object.keys(window).find(check1) || '') || {}).find(check2)}.Utils.removeDa`) || DiscordConstants.NOOP)({}) + } finally { + Error.stackTraceLimit = ol; + Error.captureStackTrace = o; + } + PluginUtilities.addStyle( + this.short + '-CSS', + ` + .BIV-left, + .BIV-right { + position: absolute; + top: 90px; + bottom: 90px; + width: 90px; + background-color: transparent; + display: flex; + justify-content: center; + align-items: center; + transition: all 0.25s ease-in-out; + color: gray; + } + .BIV-disabled { + color: #4d4d4d; + } + .BIV-left.BIV-inactive, + .BIV-right.BIV-inactive { + opacity: 0; + } + .BIV-info.BIV-inactive { + opacity: 0.65; + } + .BIV-left:not(.BIV-disabled):hover, + .BIV-right:not(.BIV-disabled):hover { + background-color: hsla(0, 0%, 49%, 0.2); + color: #fff; + } + .BIV-left { + left: 0; + } + .BIV-right { + right: 0; + } + .BIV-left > svg, + .BIV-right > svg { + width: 30px; + height: 30px; + } + .BIV-info { + position: absolute; + left: 18px; + bottom: 12px; + height: 42px; + transition: opacity 0.35s ease-in-out; + } + .theme-light .BIV-info { + color: white; + } + .BIV-info-extra { + left: unset; + right: 12px; + height: unset; + } + .BIV-info-extra > table { + width: 200px; + } + .BIV-info-extra tr > td:nth-child(2) { + text-align: end; + } + .BIV-info-wrapper { + bottom: 0; + position: absolute; + white-space: nowrap; + } + .BIV-info-wrapper > .BIV-info-wrapper-text { + display: flex; + align-items: center; + } + .BIV-requesting { + display: flex; + margin-left: 5px; + } + .BIV-requesting > svg[name='Nova_Search'], + .BIV-requesting > svg[name='LeftCaret'], + .BIV-requesting > svg[name='RightCaret'] { + width: 16px; + height: 16px; + } + .BIV-zoom-backdrop, + .biv-overlay { + width: 100%; + height: 100%; + position: absolute; + } + .BIV-inactive { + transition: opacity 1s ease-in-out; + } + .BIV-hidden { + opacity: 0; + } + .BIV-info-wrapper .${XenoLib.getClass('header username')} { + max-width: 900px; + overflow-x: hidden; + margin-right: 0.25rem; + } + .biv-overlay { + pointer-events: none; + z-index: 1002; + } + .biv-overlay > * { + pointer-events: all; + } + @keyframes BIV-spin { + 0% { + transform: rotate(0); + } + to { + transform: rotate(1turn); + } + } + .BIV-searching-icon-spin { + animation: BIV-spin 2s linear infinite; + } + .BIV-zoom-lens { + overflow: hidden; + cursor: none; + border: solid #0092ff; + border-width: thin; + } + .BIV-zoom-lens-round { + border-radius: 50%; + border: 2px solid #0092ff; + } + .BIV-zoom-backdrop { + background: rgba(0, 0, 0, 0.4); + } + .BIV-text-bold { + font-weight: 600; + } + .theme-light .BIV-text-bold { + color: white; + } + .${WebpackModules.find(e => Object.keys(e).length === 2 && e.modal && e.inner).modal.split(' ')[0]} > .${WebpackModules.find(e => Object.keys(e).length === 2 && e.modal && e.inner).inner.split(' ')[0]} > .${XenoLib.getSingleClass('imageZoom imageWrapper')} { + display: table; /* lol */ + } + ` + ); + } + + onStop() { + this.promises.state.cancelled = true; + Patcher.unpatchAll(); + Dispatcher.unsubscribe('MESSAGE_DELETE', this.handleMessageDelete); + Dispatcher.unsubscribe('MESSAGE_DELETE_BULK', this.handlePurge); + Dispatcher.unsubscribe('BIV_LENS_WH_CHANGE', this.handleWHChange); + PluginUtilities.removeStyle(this.short + '-CSS'); + if (overlayDOMNode) overlayDOMNode.remove(); + overlayDOMNode = null; + } + + buildSetting(data) { + if (data.type === 'switchbeta') { + data.type = 'switch'; + data.name = [data.name, React.createElement('sup', { className: BetaClasses.beta }, 'BETA')]; + } + return XenoLib.buildSetting(data); + } + + saveSettings(category, setting, value) { + super.saveSettings(category, setting, value); + if (category === 'chat') { + this._MA.forceUpdateAll(); + } + } + + saveHiddenSettings() { + PluginUtilities.saveData(this.name, 'hidden', this.hiddenSettings); + } + + handleMessageDelete(e) { + const { channelId, id: messageId } = e; + stripDeletedMessage(channelId, messageId); + } + handlePurge(e) { + const { channelId, ids: messageIds } = e; + stripPurgedMessages(channelId, messageIds); + } + handleWHChange({ value }) { + this.hiddenSettings.panelWH = value; + this.saveHiddenSettings(); + } + + fetchFilename(url) { + try { + if (url.indexOf('//giphy.com/gifs/') !== -1) url = `https://i.giphy.com/media/${url.match(/-([^-]+)$/)[1]}/giphy.gif`; + const match = url.match(/(?:\/)([^\/]+?)(?:(?:\.)([^.\/?:]+)){0,1}(?:[^\w\/\.]+\w+){0,1}(?:(?:\?[^\/]+){0,1}|(?:\/){0,1})$/); + let name = match[1]; + let extension = match[2] || '.png'; + if (url.indexOf('//media.tenor.co') !== -1) { + extension = name; + name = url.match(/\/\/media.tenor.co\/[^\/]+\/([^\/]+)\//)[1]; + } else if (url.indexOf('//i.giphy.com/media/') !== -1) name = url.match(/\/\/i\.giphy\.com\/media\/([^\/]+)\//)[1]; + return `${name}.${extension}`; + } catch (err) { + Logger.stacktrace('Failed to fetch filename', url, err); + return 'unknown.png'; + } + } + + /* PATCHES */ + + patchAll() { + Utilities.suppressErrors(this.patchMessageAccessories.bind(this), 'MessageAccessories patches')(this.promises.state); + Utilities.suppressErrors(this.patchLazyImageZoomable.bind(this), 'LazyImageZoomable patches')(); + Utilities.suppressErrors(this.patchImageModal.bind(this), 'ImageModal patches')(); + Utilities.suppressErrors(this.patchLazyImage.bind(this), 'LazyImage patches')(); + Utilities.suppressErrors(this.patchImageScaling.bind(this), 'image scaling patches')(); + } + + patchLazyImageZoomable() { + const patchKey = DiscordModules.KeyGenerator(); + const MaskedLink = WebpackModules.getByDisplayName('MaskedLink'); + const renderLinkComponent = props => React.createElement(MaskedLink, props); + const Modals = WebpackModules.getByProps('ModalRoot'); + const ImageModalClasses = WebpackModules.find(m => typeof m.image === 'string' && typeof m.modal === 'string' && !m.content && !m.card) || WebpackModules.getByProps('modal', 'image'); + Patcher.before(WebpackModules.getByDisplayName('LazyImageZoomable').prototype, 'render', (_this, _, ret) => { + if (_this.onZoom.__BIV_patched !== patchKey) { + _this.onZoom = (e, n) => { + let isSearch = e.target; + while (isSearch && typeof isSearch.className === 'string' && isSearch.className.indexOf('searchResultMessage') === -1) isSearch = isSearch.parentElement; + isSearch = !!isSearch; + e.preventDefault(); + if (e.currentTarget instanceof HTMLElement) e.currentTarget.blur(); + e = null; + const original = _this.props.original || _this.props.src; + ModalStack.openModal(e => { + try { + return React.createElement( + /* this safety net should prevent any major issues or crashes, in theory + UPDATE: 21.7.2020 the theory was wrong, I'm an idiot and set the state to an invalid one immediately + causing another crash and crashing the entire client! + */ + ErrorCatcher, + { + label: 'Image modal', + onError: (_, level) => { + if (level < 2) XenoLib.Notifications.error(`[${this.name}] Internal error, options will not show. If you repeatedly see this, join my support server, open up console (CTRL + SHIFT + I > click console) and screenshot any errors.`, { timeout: 0 }); + if (level > 1) e.onClose(); + }, + fallback: [ + React.createElement( + Modals.ModalRoot, + { className: ImageModalClasses.modal, ...e, size: Modals.ModalSize.DYNAMIC }, + React.createElement( + ImageModal, + Object.assign( + { + original, + src: _this.props.src, + width: _this.props.width, + height: _this.props.height, + animated: _this.props.animated, + children: _this.props.children, + placeholder: n.placeholder, + isTrusted: TrustedStore.isTrustedDomain(original), + onClickUntrusted: _this.onClickUntrusted, + renderLinkComponent, + className: ImageModalClasses.image, + shouldAnimate: true + }, + e + ) + ) + ) + ] + }, + React.createElement( + Modals.ModalRoot, + { className: ImageModalClasses.modal, ...e, size: Modals.ModalSize.DYNAMIC }, + React.createElement( + RichImageModal, + Object.assign( + { + original, + src: _this.props.src, + width: _this.props.width, + height: _this.props.height, + animated: _this.props.animated, + children: _this.props.children, + placeholder: n.placeholder, + isTrusted: TrustedStore.isTrustedDomain(original), + onClickUntrusted: _this.onClickUntrusted, + renderLinkComponent, + __BIV_data: _this.props.__BIV_data, + __BIV_index: _this.props.__BIV_data ? _this.props.__BIV_data.images.findIndex(m => m.src === _this.props.src) : -1, + __BIV_isSearch: isSearch, + __BIV_settings: this.settings, + className: ImageModalClasses.image, + shouldAnimate: true + }, + e + ) + ) + ) + ); + } catch (err) { + /* juuuuust in case, modal crashes can be brutal */ + Logger.stacktrace('Error creating image modal', err); + e.onClose(); + return null; + } + }); + }; + _this.onZoom.__BIV_patched = patchKey; + } + }); + } + + async patchMessageAccessories(promiseState) { + const selector = `.${XenoLib.getSingleClass('embedWrapper container')}`; + const MessageAccessories = await ReactComponents.getComponentByName('MessageAccessories', selector); + if (!MessageAccessories.selector) MessageAccessories.selector = selector; + if (promiseState.cancelled) return; + Patcher.before(MessageAccessories.component.prototype, 'render', _this => { + _this.__BIV_data = { + images: extractImages(_this.props.message), + messageId: _this.props.message.id + }; + }); + Patcher.before(MessageAccessories.component.prototype, 'renderEmbeds', _this => { + if (!_this.renderEmbed.__BIV_patched) { + const oRenderEmbed = _this.renderEmbed; + _this.renderEmbed = function (e, n) { + const oRenderImageComponent = n.renderImageComponent; + if (!oRenderImageComponent.__BIV_patched) { + n.renderImageComponent = function (a) { + a.__BIV_data = _this.__BIV_data; + return oRenderImageComponent(a); + }; + n.renderImageComponent.__BIV_patched = true; + } + return oRenderEmbed(e, n); + }; + _this.renderEmbed.__BIV_patched = true; + } + }); + Patcher.after(MessageAccessories.component.prototype, 'renderAttachments', (_this, _, ret) => { + if (!ret) return; + for (let attachment of ret) { + let props = Utilities.getNestedProp(attachment, 'props.children.props'); + if (!props) continue; + const oRenderImageComponent = props.renderImageComponent; + props.renderImageComponent = function (e) { + e.__BIV_data = _this.__BIV_data; + return oRenderImageComponent(e); + }; + attachment = null; + props = null; + } + ret = null; + }); + MessageAccessories.forceUpdateAll(); + this._MA = MessageAccessories; + } + + patchImageModal() { + /* shared code + these patches are for displaying image info + but the same code is shared with RichImageModal + */ + Patcher.after(ImageModal.prototype, 'handleMouseMove', _this => { + if (_this.state.controlsInactive) { + _this.setState({ controlsInactive: false }); + } + if (_this.state.controlsHovered) _this._controlsInactiveDelayedCall.cancel(); + else _this._controlsInactiveDelayedCall.delay(); + }); + /* https://stackoverflow.com/questions/10420352/ */ + function humanFileSize(bytes, si) { + const thresh = si ? 1000 : 1024; + if (Math.abs(bytes) < thresh) return `${bytes} B`; + const units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let u = -1; + do { + bytes /= thresh; + ++u; + } while (Math.abs(bytes) >= thresh && u < units.length - 1); + return `${bytes.toFixed(1)} ${units[u]}`; + } + Patcher.after(ImageModal.prototype, 'requestImageInfo', (_this, [props, equalRatio]) => { + const original = (props && props.original) || _this.props.original; + const src = (props && props.src) || _this.props.src; + const width = (props && props.width) || _this.props.width; + const height = (props && props.height) || _this.props.height; + const reqUrl = (() => { + const split = src.split('?')[0]; + const SaveToRedux = BdApi.getPlugin && BdApi.getPlugin('SaveToRedux'); + const needsSize = src.substr(src.indexOf('?')).indexOf('size=') !== -1; + try { + if (SaveToRedux) return SaveToRedux.formatURL(original || '', needsSize, '', '', split).url; + } catch (_) { } + return split + (needsSize ? '?size=2048' : ''); + })(); + const max = ImageUtils.zoomFit(width, height); + const ratio = getRatio(width, height, max.width, max.height); + const aa = ImageUtils.getImageSrc(src, width, height, ratio); + _this._headerRequest2 = RequestModule.head(aa, (err, res) => { + if (err || res.statusCode >= 400) return _this.setState({ imageSize: -1 }); + _this.setState({ imageSize: humanFileSize(res.headers['content-length']) }); + if (equalRatio) _this.setState({ originalImageSize: humanFileSize(res.headers['content-length']) }); + }); + if (!equalRatio) { + _this._headerRequest1 = RequestModule.head(reqUrl, (err, res) => { + if (err || res.statusCode >= 400) return _this.setState({ originalImageSize: -1 }); + _this.setState({ originalImageSize: humanFileSize(res.headers['content-length']) }); + }); + } + }); + const RequestModule = require('request'); + Patcher.after(ImageModal.prototype, 'componentDidMount', _this => { + if (!_this.state || _this.state.internalError) return; + const requestImageInfo = XenoLib._.debounce(_this.requestImageInfo.bind(_this), 750, { leading: true }); + _this.requestImageInfo = props => { + const settings = this.settings.ui; + if (!settings.infoResolution && settings.infoScale && settings.infoSize) return; + const width = (props && props.width) || _this.props.width; + const height = (props && props.height) || _this.props.height; + const max = ImageUtils.zoomFit(width, height); + const scaledRatio = getRatio(width, height, max.width, max.height); + const finalRatio = scaledRatio < 1 ? scaledRatio : 1; + if (settings.infoResolution || settings.infoScale) { + _this.setState({ + basicImageInfo: { + width: Math.ceil(width * finalRatio), + height: Math.ceil(height * finalRatio), + ratio: finalRatio + }, + imageSize: null, + originalImageSize: null + }); + } else _this.setState({ imageSize: null, originalImageSize: null }); + if (_this._headerRequest1) _this._headerRequest1.abort(); + if (_this._headerRequest2) _this._headerRequest2.abort(); + if (settings.infoSize) requestImageInfo(props, finalRatio === 1); + }; + _this.requestImageInfo(); + _this._controlsVisibleTimeout = new TimingModule.Timeout(); + _this._controlsVisibleTimeout.start(300, () => _this.setState({ controlsVisible: true })); + _this._controlsInactiveDelayedCall = new TimingModule.DelayedCall(3500, () => _this.setState({ controlsInactive: true })); + _this._controlsInactiveDelayedCall.delay(); + _this.handleMouseMove = XenoLib._.throttle(_this.handleMouseMove.bind(_this), 500); + window.addEventListener('mousemove', _this.handleMouseMove); + Dispatcher.subscribe( + 'BIV_LOAD_FULLRES', + (_this._fullresHandler = () => { + if (_this.state.showFullRes) return; + _this.setState({ + showFullRes: true + }); + }) + ); + }); + Patcher.after(ImageModal.prototype, 'componentWillUnmount', _this => { + if (!_this.state || _this.state.internalError) return; + if (_this._headerRequest1) _this._headerRequest1.abort(); + if (_this._headerRequest2) _this._headerRequest2.abort(); + if (!_this._controlsVisibleTimeout) { + /* since the BdApi is like a child on cocaine, I'll just wrap it in a try catch */ + let reloadFailed = false; + try { + BdApi.Plugins.reload(this.name); + } catch (e) { + try { + pluginModule.reloadPlugin(this.name); + } catch (e) { + reloadFailed = true; + } + } + XenoLib.Notifications.warning(`[**${this.name}**] Something's not right.. ${reloadFailed ? 'Reloading self failed..' : 'Reloading self.'}`); + } + if (_this._controlsVisibleTimeout) _this._controlsVisibleTimeout.stop(); + if (_this._controlsInactiveDelayedCall) _this._controlsInactiveDelayedCall.cancel(); + window.removeEventListener('mousemove', _this.handleMouseMove); + Dispatcher.unsubscribe('BIV_LOAD_FULLRES', _this._fullresHandler); + }); + const renderTableEntry = (val1, val2) => React.createElement('tr', {}, React.createElement('td', {}, val1), React.createElement('td', {}, val2)); + Patcher.after(ImageModal.prototype, 'render', (_this, _, ret) => { + if (!_this.state) + _this.state = { + controlsInactive: false, + controlsVisible: false, + imageSize: null, + originalImageSize: null, + basicImageInfo: null, + showFullRes: false + }; + if (this.settings.ui.loadFull) _this.state.showFullRes = true; + const imageProps = Utilities.getNestedProp(ret, 'props.children.0.props'); + if (imageProps) imageProps.__BIV_full_res = _this.state.showFullRes; + if (_this.state.internalError) return; + const settings = this.settings.ui; + const debug = this.settings.behavior.debug; + if (!settings.infoResolution && settings.infoScale && settings.infoSize && !debug) return; + const { basicImageInfo, imageSize, originalImageSize } = _this.state; + // splice in, otherwise ImageToClipboard freaks out + ret.props.children.splice( + 1, + 0, + /* portals are cool o; */ + ReactDOM.createPortal( + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-info BIV-info-extra', { 'BIV-hidden': !_this.state.controlsVisible, 'BIV-inactive': _this.state.controlsInactive && !debug }, TextElement.Colors.STANDARD) + }, + React.createElement('table', {}, settings.infoFilename || debug ? React.createElement('tr', {}, React.createElement('td', { colspan: 2 }, this.fetchFilename(_this.props.src))) : null, settings.infoResolution || debug ? renderTableEntry(basicImageInfo ? React.createElement('span', { className: _this.state.showFullRes ? TextElement.Colors.ERROR : undefined }, `${basicImageInfo.width}x${basicImageInfo.height}`) : 'NaNxNaN', `${_this.props.width}x${_this.props.height}`) : null, settings.infoSize || debug ? renderTableEntry(imageSize ? imageSize : 'NaN', originalImageSize ? (originalImageSize === imageSize ? '~' : originalImageSize) : 'NaN') : null, debug ? Object.keys(_this.state).map(key => (!XenoLib._.isObject(_this.state[key]) && key !== 'src' && key !== 'original' && key !== 'placeholder' ? renderTableEntry(key, String(_this.state[key])) : null)) : null) + ), + overlayDOMNode + ) + ); + }); + } + + patchLazyImage() { + if (NoImageZoom) return; + const LazyImage = WebpackModules.getByDisplayName('LazyImage'); + // BETA CODE! + const SectionStore = WebpackModules.find(m => m.getSection && !m.getProps && !m.getStats); + const NO_SIDEBAR = 0.666178623635432; + const SEARCH_SIDEBAR = 0.3601756956193265; + const MEMBERS_SIDEBAR = 0.49048316246120055; + Patcher.instead(LazyImage.prototype, 'handleSidebarChange', (_this, [forced]) => { + if (!this.settings.chat.resize) return; + const { state } = _this; + if (!currentChannel()) { + state.__BIV_sidebarMultiplier = null; + return; + } + const section = SectionStore.getSection(); + let newMultiplier; + if (section === 'SEARCH') newMultiplier = SEARCH_SIDEBAR; + else if (section !== 'MEMBERS' || (!DiscordModules.SelectedGuildStore.getGuildId() && currentChannel().type !== 'GROUP_DM')) newMultiplier = NO_SIDEBAR; + else newMultiplier = MEMBERS_SIDEBAR; + if (!forced && newMultiplier !== state.__BIV_sidebarMultiplier) _this.setState({ __BIV_sidebarMultiplier: newMultiplier }); + else state.__BIV_sidebarMultiplier = newMultiplier; + }); + Patcher.after(LazyImage.prototype, 'componentDidMount', _this => { + if (typeof _this.props.__BIV_index !== 'undefined' /* || _this.props.__BIV_isVideo */ || (_this.props.className && _this.props.className.indexOf('embedThumbnail') !== -1)) { + _this.handleSidebarChange = null; + return; + } + _this.handleSidebarChange = _this.handleSidebarChange.bind(_this); + SectionStore.addChangeListener(_this.handleSidebarChange); + }); + Patcher.after(LazyImage.prototype, 'componentWillUnmount', _this => { + if (!_this.handleSidebarChange) return; + SectionStore.removeChangeListener(_this.handleSidebarChange); + }); + Patcher.before(LazyImage.prototype, 'getRatio', _this => { + if (!this.settings.chat.resize) return; + if (!_this.handleSidebarChange || typeof _this.props.__BIV_index !== 'undefined' /* || _this.props.__BIV_isVideo */ || (_this.props.className && _this.props.className.indexOf('embedThumbnail') !== -1)) return; + if (typeof _this.state.__BIV_sidebarType === 'undefined') _this.handleSidebarChange(true); + if (_this.state.__BIV_sidebarMultiplier === null) return; + const scale = window.innerWidth / (window.innerWidth * window.devicePixelRatio); + _this.props.maxWidth = Math.max(Math.min(innerWidth * devicePixelRatio * _this.state.__BIV_sidebarMultiplier * (1 + (1 - devicePixelRatio)), _this.props.width * scale), 400); + _this.props.maxHeight = Math.max(Math.min(innerHeight * devicePixelRatio * 0.6777027027027027, _this.props.height * scale), 300); + }); + // BETA CODE! + + Patcher.instead(LazyImage.prototype, 'componentDidUpdate', (_this, [props, state]) => { + /* custom handler, original one caused issues with GIFs not animating */ + const animated = LazyImage.isAnimated(_this.props); + if (animated !== LazyImage.isAnimated(props)) { + if (animated) _this.observeVisibility(); + else _this.unobserveVisibility(); + } else if (state.readyState !== _this.state.readyState && animated) _this.observeVisibility(); + else if (!animated) _this.unobserveVisibility(); + }); + Patcher.instead(LazyImage.prototype, 'getSrc', (_this, [ratio, forcePng], orig) => { + if (_this.props.__BIV_full_res) return _this.props.src; + return orig(ratio, forcePng); + }); + Patcher.after(LazyImage.prototype, 'render', (_this, _, ret) => { + if (!ret) return; + if (_this.state.readyState !== 'READY' || _this.props.__BIV_isVideo) return; + /* fix scaling issues for all images */ + if (!this.settings.chat.scale && _this.props.onZoom) return; + const scale = window.innerWidth / (window.innerWidth * window.devicePixelRatio); + ret.props.width = ret.props.width * scale; + ret.props.height = ret.props.height * scale; + if (_this.props.onZoom) return; + if (_this.props.animated && ret.props.children) { + /* dirty */ + try { + ret.props.__BIV_src = ret.props.children({ size: {} }).props.src; + } catch (e) { + return; + } + } + ret.type = Image; + ret.props.__BIV_settings = this.settings.zoom; + ret.props.__BIV_animated = _this.props.animated; + ret.props.__BIV_hiddenSettings = this.hiddenSettings; + }); + Patcher.after(WebpackModules.getByDisplayName('LazyVideo').prototype, 'render', (_, __, ret) => { + if (!ret) return; + ret.props.__BIV_isVideo = true; + }); + } + + patchImageScaling() { + Patcher.instead(WebpackModules.getByProps('zoomFit'), 'zoomFit', (_, [e, t]) => ImageUtils.zoomFit(e, t)); + Patcher.instead(_ImageUtils, 'getImageSrc', (_, args) => ImageUtils.getImageSrc(...args)); + Patcher.before(_ImageUtils, 'getSizedImageSrc', (_, args) => { + const toAdd = window.innerWidth / (window.innerWidth * window.devicePixelRatio); + args[1] *= toAdd; + args[2] *= toAdd; + }); + } + + /* PATCHES */ + + showChangelog(footer) { + XenoLib.showChangelog(`${this.name} has been updated!`, this.version, this._config.changelog); + } + + getSettingsPanel() { + return this.buildSettingsPanel().append(new XenoLib.Settings.PluginFooter(this.showChangelog)).getElement(); + } + + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + }; + }; + + /* Finalize */ + + let ZeresPluginLibraryOutdated = false; + let XenoLibOutdated = false; + try { + if (global.BdApi && 'function' == typeof BdApi.getPlugin) { + const i = (i, n) => ((i = i.split('.').map(i => parseInt(i))), (n = n.split('.').map(i => parseInt(i))), !!(n[0] > i[0]) || !!(n[0] == i[0] && n[1] > i[1]) || !!(n[0] == i[0] && n[1] == i[1] && n[2] > i[2])), + n = (n, e) => n && n._config && n._config.info && n._config.info.version && i(n._config.info.version, e), + e = BdApi.getPlugin('ZeresPluginLibrary'), + o = BdApi.getPlugin('XenoLib'); + n(e, '1.2.26') && (ZeresPluginLibraryOutdated = !0), n(o, '1.3.32') && (XenoLibOutdated = !0); + } + } catch (i) { + console.error('Error checking if libraries are out of date', i); + } + + return !global.ZeresPluginLibrary || !global.XenoLib || ZeresPluginLibraryOutdated || XenoLibOutdated + ? class { + constructor() { + this._XL_PLUGIN = true; + this.start = this.load = this.handleMissingLib; + } + getName() { + return this.name.replace(/\s+/g, ''); + } + getAuthor() { + return this.author; + } + getVersion() { + return this.version; + } + getDescription() { + return this.description + ' You are missing libraries for this plugin, please enable the plugin and click Download Now.'; + } + start() { } + stop() { } + handleMissingLib() { + const a = BdApi.findModuleByProps('openModal', 'hasModalOpen'); + if (a && a.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const b = !global.XenoLib, + c = !global.ZeresPluginLibrary, + d = (b && c) || ((b || c) && (XenoLibOutdated || ZeresPluginLibraryOutdated)), + e = (() => { + let a = ''; + return b || c ? (a += `Missing${XenoLibOutdated || ZeresPluginLibraryOutdated ? ' and outdated' : ''} `) : (XenoLibOutdated || ZeresPluginLibraryOutdated) && (a += `Outdated `), (a += `${d ? 'Libraries' : 'Library'} `), a; + })(), + f = (() => { + let a = `The ${d ? 'libraries' : 'library'} `; + return b || XenoLibOutdated ? ((a += 'XenoLib '), (c || ZeresPluginLibraryOutdated) && (a += 'and ZeresPluginLibrary ')) : (c || ZeresPluginLibraryOutdated) && (a += 'ZeresPluginLibrary '), (a += `required for ${this.name} ${d ? 'are' : 'is'} ${b || c ? 'missing' : ''}${XenoLibOutdated || ZeresPluginLibraryOutdated ? (b || c ? ' and/or outdated' : 'outdated') : ''}.`), a; + })(), + g = BdApi.findModuleByDisplayName('Text'), + h = BdApi.findModuleByDisplayName('ConfirmModal'), + i = () => BdApi.alert(e, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, f), `Due to a slight mishap however, you'll have to download the libraries yourself. This is not intentional, something went wrong, errors are in console.`, c || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null, b || XenoLibOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=3169', target: '_blank' }, 'Click here to download XenoLib')) : null)); + if (!a || !h || !g) return console.error(`Missing components:${(a ? '' : ' ModalStack') + (h ? '' : ' ConfirmationModalComponent') + (g ? '' : 'TextElement')}`), i(); + class j extends BdApi.React.PureComponent { + constructor(a) { + super(a), (this.state = { hasError: !1 }), (this.componentDidCatch = a => (console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), 'function' == typeof this.props.onError && this.props.onError(a))), (this.render = () => (this.state.hasError ? null : this.props.children)); + } + } + let k = !1, + l = !1; + const m = a.openModal( + b => { + if (l) return null; + try { + return BdApi.React.createElement( + j, + { label: 'missing dependency modal', onError: () => (a.closeModal(m), i()) }, + BdApi.React.createElement( + h, + Object.assign( + { + header: e, + children: BdApi.React.createElement(g, { size: g.Sizes.SIZE_16, children: [`${f} Please click Download Now to download ${d ? 'them' : 'it'}.`] }), + red: !1, + confirmText: 'Download Now', + cancelText: 'Cancel', + onCancel: b.onClose, + onConfirm: () => { + if (k) return; + k = !0; + const b = require('request'), + c = require('fs'), + d = require('path'), + e = BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, + f = () => { + (global.XenoLib && !XenoLibOutdated) || + b('https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js', (b, f, g) => { + try { + if (b || 200 !== f.statusCode) return a.closeModal(m), i(); + c.writeFile(d.join(e, '1XenoLib.plugin.js'), g, () => { }); + } catch (b) { + console.error('Fatal error downloading XenoLib', b), a.closeModal(m), i(); + } + }); + }; + !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated + ? b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, g, h) => { + try { + if (b || 200 !== g.statusCode) return a.closeModal(m), i(); + c.writeFile(d.join(e, '0PluginLibrary.plugin.js'), h, () => { }), f(); + } catch (b) { + console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(m), i(); + } + }) + : f(); + } + }, + b, + { onClose: () => { } } + ) + ) + ); + } catch (b) { + return console.error('There has been an error constructing the modal', b), (l = !0), a.closeModal(m), i(), null; + } + }, + { modalKey: `${this.name}_DEP_MODAL` } + ); + } + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config)); + })(); + + /*@end@*/ \ No newline at end of file diff --git a/BetterTypingUsers.plugin.js b/BetterTypingUsers.plugin.js new file mode 100644 index 0000000..c42cba1 --- /dev/null +++ b/BetterTypingUsers.plugin.js @@ -0,0 +1,463 @@ +//META{"name":"BetterTypingUsers","source":"https://gitdab.com/hana/lightcock/src/branch/master/BetterTypingUsers.plugin.js","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=BetterTypingUsers","authorId":"287977955240706060","invite":"9jDgyVjTVX","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.\nJust reload Discord with Ctrl+R.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!\nJust reload Discord with Ctrl+R.', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); + +@else@*/ +/* + * Copyright © 2019-2020, _Lighty_ + * All rights reserved. + * Code may not be redistributed, modified or otherwise taken without explicit permission. + */ +module.exports = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'BetterTypingUsers', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: 'LightyPon', + twitter_username: '' + } + ], + version: '1.0.1', + description: 'Replaces "Several people are typing" with who is actually typing, plus "x others" if it can\'t fit. Number of shown people typing can be changed.', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/BetterTypingUsers/BetterTypingUsers.plugin.js' + }, + changelog: [ + { + title: 'fixed', + type: 'fixed', + items: ['Changed to module.exports because useless backwards incompatbile changes are the motto for BBD apparently.'] + } + ], + defaultConfig: [ + { + name: 'Max visible typing users', + id: 'maxVisible', + type: 'slider', + value: 5, + min: 3, + max: 20, + markers: Array.from(Array(18), (_, i) => i + 3), + stickToMarkers: true + }, + { + name: 'Previews', + type: 'preview' + } + ], + strings: { + en: { AND_1_OTHER: ' and 1 other', AND_X_OTHERS: ' and ${count} others', AND: ' and ', ARE_TYPING: ' are typing...' }, + de: { AND_1_OTHER: ' und 1 andere', AND_X_OTHERS: ' und ${count} andere', AND: ' und ', ARE_TYPING: ' schreiben...' }, + da: { AND_1_OTHER: ' og 1 anden', AND_X_OTHERS: ' og ${count} andre', AND: ' og ', ARE_TYPING: ' skriver...' }, + es: { AND_1_OTHER: ' y 1 otro', AND_X_OTHERS: ' y otros ${count}', AND: ' y ', ARE_TYPING: ' están escribiendo...' }, + fr: { AND_1_OTHER: ' et 1 autre', AND_X_OTHERS: ' et ${count} autres', AND: ' et ', ARE_TYPING: ' écrivent...' }, + hr: { AND_1_OTHER: ' i 1 drugi', AND_X_OTHERS: ' i ${count} drugih', AND: ' i ', ARE_TYPING: ' pišu...' }, + it: { AND_1_OTHER: ' e 1 altro', AND_X_OTHERS: ' e altri ${count}', AND: ' e ', ARE_TYPING: ' stanno scrivendo...' }, + tr: { AND_1_OTHER: ' ve 1 kişi daha', AND_X_OTHERS: ' ve ${count} kişi daha', AND: ' ve ', ARE_TYPING: ' yazıyor...' } + } + }; + + /* Build */ + const buildPlugin = ([Plugin, Api]) => { + const { ContextMenu, EmulatedTooltip, Toasts, Settings, Popouts, Modals, Utilities, WebpackModules, Filters, DiscordModules, ColorConverter, DOMTools, DiscordClasses, DiscordSelectors, ReactTools, ReactComponents, DiscordAPI, Logger, Patcher, PluginUpdater, PluginUtilities, DiscordClassModules, Structs } = Api; + const { React, ModalStack, ContextMenuActions, ContextMenuItem, ContextMenuItemsGroup, ReactDOM, ChannelStore, GuildStore, UserStore, DiscordConstants, Dispatcher, GuildMemberStore, GuildActions, SwitchRow, EmojiUtils, RadioGroup, Permissions, TextElement, FlexChild, PopoutOpener, Textbox, RelationshipStore, UserSettingsStore } = DiscordModules; + + const NameUtils = WebpackModules.getByProps('getName'); + + const CUser = WebpackModules.getByPrototypes('getAvatarSource', 'isLocalBot'); + const CChannel = WebpackModules.getByPrototypes('isGroupDM', 'isMultiUserDM'); + const L337 = (() => { + try { + return new CChannel({ id: '1337' }); + } catch (e) { + Logger.stacktrace('Failed to create 1337 channel', e); + } + })(); + + let CTypingUsers = (() => { + try { + const WrappedTypingUsers = WebpackModules.find(m => m.displayName && m.displayName.indexOf('TypingUsers') !== -1); + return new WrappedTypingUsers({ channel: L337 }).render().type; + } catch (e) { + Logger.stacktrace('Failed to get TypingUsers!', e); + return null; + } + })(); + + const ComponentDispatch = (() => { + try { + return WebpackModules.getByProps('ComponentDispatch').ComponentDispatch; + } catch (e) { + Logger.stacktrace('Failed to get ComponentDispatch', e); + } + })(); + + class CTypingUsersPreview extends React.PureComponent { + constructor(props) { + super(props); + this.forceUpdate = this.forceUpdate.bind(this); + const iUsers = UserStore.getUsers(); + for (let i = 0; i < 20; i++) iUsers[(1337 + i).toString()] = new CUser({ username: `User ${i + 1}`, id: (1337 + i).toString(), discriminator: '9999' }); + } + componentDidMount() { + ComponentDispatch.subscribe('BTU_SETTINGS_UPDATED', this.forceUpdate); + } + componentWillUnmount() { + ComponentDispatch.unsubscribe('BTU_SETTINGS_UPDATED', this.forceUpdate); + const iUsers = UserStore.getUsers(); + for (let i = 0; i < 20; i++) delete iUsers[(1337 + i).toString()]; + } + renderTyping(num) { + const typingUsers = {}; + for (let i = 0; i < num; i++) typingUsers[(1337 + i).toString()] = 1; + return React.createElement(CTypingUsers, { + channel: L337, + guildId: '', + isFocused: true, + slowmodeCooldownGuess: 0, + theme: UserSettingsStore.theme, + typingUsers + }); + } + render() { + return React.createElement( + 'div', + { + className: 'BTU-preview' + }, + this.renderTyping(4), + this.renderTyping(6), + this.renderTyping(20) + ); + } + } + + class TypingUsersPreview extends Settings.SettingField { + constructor(name, note) { + super(name, note, null, CTypingUsersPreview); + } + } + + /* since XenoLib is absent from this plugin (since it serves no real purpose), + we can only hope the user doesn't rename the plugin.. + */ + return class BetterTypingUsers extends Plugin { + constructor() { + super(); + try { + WebpackModules.getByProps('openModal', 'hasModalOpen').closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + } + onStart() { + if (window.Lightcock && window.XenoLib) XenoLib.Notifications.warning(`[${this.getName()}] Lightcord is an unofficial and unsafe client with stolen code that is falsely advertising that it is safe, Lightcord has allowed the spread of token loggers hidden within plugins redistributed by them, and these plugins are not made to work on it. Your account is very likely compromised by malicious people redistributing other peoples plugins, especially if you didn't download this plugin from [GitHub](https://github.com/1Lighty/BetterDiscordPlugins/edit/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js), you should change your password immediately. Consider using a trusted client mod like [BandagedBD](https://rauenzi.github.io/BetterDiscordApp/) or [Powercord](https://powercord.dev/) to avoid losing your account.`, { timeout: 0 }); + this.promises = { state: { cancelled: false } }; + this.patchAll(); + PluginUtilities.addStyle( + this.short + '-CSS', + ` + .BTU-preview > .${WebpackModules.getByProps('slowModeIcon', 'typing').typing.split(' ')[0]} { + position: unset !important; + } + ` + ); + DiscordConstants.MAX_TYPING_USERS = 99; + /* theoretical max is 5 users typing at once.. welp */ + } + + onStop() { + this.promises.state.cancelled = true; + Patcher.unpatchAll(); + PluginUtilities.removeStyle(this.short + '-CSS'); + } + + /* zlib uses reference to defaultSettings instead of a cloned object, which sets settings as default settings, messing everything up */ + loadSettings(defaultSettings) { + return PluginUtilities.loadSettings(this.name, Utilities.deepclone(this.defaultSettings ? this.defaultSettings : defaultSettings)); + } + + buildSetting(data) { + if (data.type === 'preview') return new TypingUsersPreview(data.name, data.note); + return super.buildSetting(data); + } + + saveSettings(_, setting, value) { + super.saveSettings(_, setting, value); + ComponentDispatch.safeDispatch('BTU_SETTINGS_UPDATED'); + } + + filterTypingUsers(typingUsers) { + return Object.keys(typingUsers) + .filter(e => e != DiscordAPI.currentUser.id) + .filter(e => !RelationshipStore.isBlocked(e)) + .map(e => UserStore.getUser(e)) + .filter(e => e != null); + } + + /* PATCHES */ + + patchAll() { + Utilities.suppressErrors(this.patchBetterRoleColors.bind(this), 'BetterRoleColors patch')(); + Utilities.suppressErrors(this.patchTypingUsers.bind(this), 'TypingUsers patch')(this.promises.state); + } + + patchBetterRoleColors() { + const BetterRoleColors = BdApi.getPlugin('BetterRoleColors'); + if (!BetterRoleColors) return; + /* stop errors */ + /* modify BRCs behavior so it won't unexpectedly try to modify an entry that does not exist + by simply limiting it to the max number of usernames visible in total + */ + Patcher.after(BetterRoleColors, 'filterTypingUsers', (_this, __, ret) => ret.slice(0, this.settings.maxVisible)); + } + + async patchTypingUsers(promiseState) { + const TypingUsers = await ReactComponents.getComponentByName('TypingUsers', DiscordSelectors.Typing.typing); + if (!TypingUsers.selector) TypingUsers.selector = DiscordSelectors.Typing.typing; + const TypingTextClassname = WebpackModules.getByProps('typing', 'text').text.split(' ')[0]; + if (promiseState.cancelled) return; + if (!CTypingUsers) CTypingUsers = typingUsers.component; /* failsafe */ + /* use `instead` so that we modify the return before BetterRoleColors */ + /* Patcher.after(TypingUsers.component.prototype, 'componentDidUpdate', (_this, [props, state], ret) => { + const filtered1 = this.filterTypingUsers(_this.props.typingUsers); + const filtered2 = this.filterTypingUsers(props.typingUsers); + if (filtered1.length !== filtered2.length || _this.state.numLess === state.numLess) { + _this.state.numLess = 0; + _this.triedLess = false; + _this.triedMore = false; + } + }); */ + Patcher.instead(TypingUsers.component.prototype, 'render', (_this, _, orig) => { + /* if (!_this.state) _this.state = { numLess: 0 }; */ + const ret = orig(); + if (!ret) { + /* _this.state.numLess = 0; */ + return ret; + } + const filtered = this.filterTypingUsers(_this.props.typingUsers); + if (filtered.length <= 3) return ret; + /* ret.ref = e => { + _this.__baseRef = e; + if (!e) return; + if (!_this.__textRef) return; + _this.maxWidth = parseInt(getComputedStyle(_this.__baseRef.parentElement).width) - (_this.__textRef.offsetLeft + parseInt(getComputedStyle(_this.__textRef)['margin-left']) - _this.__baseRef.offsetLeft); + if (_this.__textRef.scrollWidth > _this.maxWidth) { + if (_this.triedMore) return; + if (filtered.length - _this.state.numLess <= 3) return; + _this.setState({ numLess: _this.state.numLess + 1 }); + } + }; */ + const typingUsers = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf(TypingTextClassname) !== -1); + if (!typingUsers) return ret; + /* if (typeof _this.state.numLess !== 'number') _this.state.numLess = 0; + typingUsers.ref = e => { + _this.__textRef = e; + }; */ + typingUsers.props.children = []; + /* I don't think this method works for every language..? */ + for (let i = 0; i < filtered.length; i++) { + if (this.settings.maxVisible /* filtered.length - _this.state.numLess */ === i) { + const others = filtered.length - i; + if (others === 1) typingUsers.props.children.push(this.strings.AND_1_OTHER); + else typingUsers.props.children.push(Utilities.formatTString(this.strings.AND_X_OTHERS, { count: others })); + break; + } else if (i === filtered.length - 1) typingUsers.props.children.push(this.strings.AND); + else if (i !== 0) typingUsers.props.children.push(', '); + const name = NameUtils.getName(_this.props.guildId, _this.props.channel.id, filtered[i]); + typingUsers.props.children.push(React.createElement('strong', {}, name)); + } + typingUsers.props.children.push(this.strings.ARE_TYPING); + return ret; + }); + TypingUsers.forceUpdateAll(); + } + + /* PATCHES */ + + getSettingsPanel() { + return this.buildSettingsPanel().getElement(); + } + + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + }; + }; + + /* Finalize */ + + let ZeresPluginLibraryOutdated = false; + try { + if (global.BdApi && 'function' == typeof BdApi.getPlugin) { + const a = (c, a) => ((c = c.split('.').map(b => parseInt(b))), (a = a.split('.').map(b => parseInt(b))), !!(a[0] > c[0])) || !!(a[0] == c[0] && a[1] > c[1]) || !!(a[0] == c[0] && a[1] == c[1] && a[2] > c[2]), + b = BdApi.getPlugin('ZeresPluginLibrary'); + ((b, c) => b && b._config && b._config.info && b._config.info.version && a(b._config.info.version, c))(b, '1.2.23') && (ZeresPluginLibraryOutdated = !0); + } + } catch (e) { + console.error('Error checking if ZeresPluginLibrary is out of date', e); + } + + return !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated + ? class { + constructor() { + this._config = config; + this.start = this.load = this.handleMissingLib; + } + getName() { + return this.name.replace(/\s+/g, ''); + } + getAuthor() { + return this.author; + } + getVersion() { + return this.version; + } + getDescription() { + return this.description + ' You are missing ZeresPluginLibrary for this plugin, please enable the plugin and click Download Now.'; + } + start() { } + stop() { } + handleMissingLib() { + const a = BdApi.findModuleByProps('openModal', 'hasModalOpen'); + if (a && a.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const b = !global.ZeresPluginLibrary, + c = ZeresPluginLibraryOutdated ? 'Outdated Library' : 'Missing Library', + d = `The Library ZeresPluginLibrary required for ${this.name} is ${ZeresPluginLibraryOutdated ? 'outdated' : 'missing'}.`, + e = BdApi.findModuleByDisplayName('Text'), + f = BdApi.findModuleByDisplayName('ConfirmModal'), + g = () => BdApi.alert(c, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, d), `Due to a slight mishap however, you'll have to download the libraries yourself. This is not intentional, something went wrong, errors are in console.`, b || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null)); + if (!a || !f || !e) return console.error(`Missing components:${(a ? '' : ' ModalStack') + (f ? '' : ' ConfirmationModalComponent') + (e ? '' : 'TextElement')}`), g(); + class h extends BdApi.React.PureComponent { + constructor(a) { + super(a), (this.state = { hasError: !1 }); + } + componentDidCatch(a) { + console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), 'function' == typeof this.props.onError && this.props.onError(a); + } + render() { + return this.state.hasError ? null : this.props.children; + } + } + let i = !1, + j = !1; + const k = a.openModal( + b => { + if (j) return null; + try { + return BdApi.React.createElement( + h, + { + label: 'missing dependency modal', + onError: () => { + a.closeModal(k), g(); + } + }, + BdApi.React.createElement( + f, + Object.assign( + { + header: c, + children: BdApi.React.createElement(e, { size: e.Sizes.SIZE_16, children: [`${d} Please click Download Now to download it.`] }), + red: !1, + confirmText: 'Download Now', + cancelText: 'Cancel', + onCancel: b.onClose, + onConfirm: () => { + if (i) return; + i = !0; + const b = require('request'), + c = require('fs'), + d = require('path'); + b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, e, f) => { + try { + if (b || 200 !== e.statusCode) return a.closeModal(k), g(); + c.writeFile(d.join(BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, '0PluginLibrary.plugin.js'), f, () => { }); + } catch (b) { + console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(k), g(); + } + }); + } + }, + b, + { onClose: () => { } } + ) + ) + ); + } catch (b) { + return console.error('There has been an error constructing the modal', b), (j = !0), a.closeModal(k), g(), null; + } + }, + { modalKey: `${this.name}_DEP_MODAL` } + ); + } + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config)); + })(); + + /*@end@*/ \ No newline at end of file diff --git a/BetterUnavailableGuilds.plugin.js b/BetterUnavailableGuilds.plugin.js new file mode 100644 index 0000000..3ce4290 --- /dev/null +++ b/BetterUnavailableGuilds.plugin.js @@ -0,0 +1,522 @@ +//META{"name":"BetterUnavailableGuilds","source":"https://gitdab.com/hana/lightcock/src/branch/master/BetterUnavailableGuilds.plugin.js/","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=BetterUnavailableGuilds","authorId":"287977955240706060","invite":"9jDgyVjTVX","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.\nJust go to settings, plugins and enable me.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!\nJust go to settings, plugins and enable me!', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); + +@else@*/ +/* + * Copyright © 2020, _Lighty_ + * All rights reserved. + * Code may not be redistributed, modified or otherwise taken without explicit permission. + */ +module.exports = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'BetterUnavailableGuilds', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: '1Lighty', + twitter_username: '' + } + ], + version: '0.2.9', + description: 'Force Discord to show server icons of unavailable servers, instead of "1 server is unavailable" and enable interaction with the server (ability to leave the server, move it around, etc).', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/BetterUnavailableGuilds/BetterUnavailableGuilds.plugin.js' + }, + changelog: [ + { + title: 'fixed', + type: 'fixed', + items: ['Fixed not restoring the servers on startup.'] + } + ], + defaultConfig: [ + { + type: 'category', + id: 'guilds', + name: 'Guilds', + collapsible: true, + shown: true, + settings: [ + { + type: 'textbox', + name: 'Add guild using data', + note: 'Press enter to add' + }, + { + type: 'guildslist', + name: 'Click to copy guild data to share to people' + } + ] + } + ] + }; + + /* Build */ + const buildPlugin = ([Plugin, Api]) => { + const { Utilities, WebpackModules, DiscordModules, Patcher, PluginUtilities, DiscordAPI, Settings, Toasts } = Api; + const { Dispatcher, GuildStore, React, ModalStack } = DiscordModules; + const GuildAvailabilityStore = WebpackModules.getByProps('unavailableGuilds'); + const _ = WebpackModules.getByProps('bindAll', 'debounce'); + + const FsModule = require('fs'); + // I would say "fuck ED", but it won't even compile on ED due to their piss poor BD/BBD support, lol + const pluginConfigFile = require('path').resolve(BdApi.Plugins.folder, config.info.name + '.config.json'); + + const loadData = (key, defaults) => { + const cloned = _.cloneDeep(defaults); + try { + if (pluginConfigFile) { + if (FsModule.existsSync(pluginConfigFile)) { + return Object.assign(cloned, JSON.parse(FsModule.readFileSync(pluginConfigFile))[key]); + } else { + return cloned; + } + } else { + return Object.assign(cloned, BdApi.loadData(config.info.name, key)); + } + } catch (e) { + return cloned; + } + }; + + const copyToClipboard = WebpackModules.getByProps('copy').copy; /* Possible error in future, TODO: safeguard */ + const GuildIconWrapper = WebpackModules.getByDisplayName('GuildIconWrapper'); + const ListClassModule = WebpackModules.getByProps('listRowContent', 'listAvatar'); + const ListScrollerClassname = WebpackModules.getByProps('listScroller').listScroller; /* Possible error in future, TODO: safeguard */ + const VerticalScroller = WebpackModules.getByDisplayName('VerticalScroller'); + const Clickable = WebpackModules.getByDisplayName('Clickable'); + + /* TODO: proper name for the classes */ + class GL extends React.PureComponent { + renderGuild(guild) { + if (!guild) return null; + return React.createElement( + Clickable, + { + onClick: () => { + copyToClipboard(JSON.stringify({ id: guild.id, icon: guild.icon || undefined, name: guild.name, owner_id: guild.ownerId, joined_at: guild.joinedAt.valueOf() })); + Toasts.success(`Copied ${guild.name}!`); + }, + className: 'BUG-guild-icon' + }, + React.createElement(GuildIconWrapper, { + guild, + showBadge: !0, + className: !guild.icon ? ListClassModule.guildAvatarWithoutIcon : '', + size: GuildIconWrapper.Sizes.LARGE + }) + ); + } + render() { + return React.createElement( + VerticalScroller, + { + fade: true, + className: ListScrollerClassname, + style: { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap' + } + }, + Object.values(GuildStore.getGuilds()).map(this.renderGuild) + ); + } + } + + class TB extends DiscordModules.Textbox { + render() { + const ret = super.render(); + const props = Utilities.findInReactTree(ret, e => e && e.onEnterPressed); + props.onKeyDown = e => { + if (e.keyCode !== 13) return; + try { + const parsed = JSON.parse(this.props.value); + ['id', 'name', 'owner_id', 'joined_at'].forEach(prop => { + if (!parsed.hasOwnProperty(prop) || typeof parsed[prop] === 'undefined') throw `Malformed guild data (${prop})`; + }); + if (typeof parsed.name !== 'string' || typeof parsed.owner_id !== 'string' || /\\d+$/.test(parsed.owner_id)) throw 'Malformed guild data'; + this.props.onEnterPressed(parsed); + this.props.onChange(''); + } catch (err) { + Toasts.error(`Failed to parse: ${err.message || err}`); + } + }; + return ret; + } + } + + class Textbox extends Settings.SettingField { + constructor(name, note, onChange, options = {}) { + super(name, note, () => { }, TB, { + onChange: textbox => value => { + textbox.props.value = value; + textbox.forceUpdate(); + }, + value: '', + placeholder: options.placeholder ? options.placeholder : '', + onEnterPressed: onChange + }); + } + } + + class GuildDataCopier extends Settings.SettingField { + constructor(name, note) { + super(name, note, () => { }, GL, {}); + } + } + + return class BetterUnavailableGuilds extends Plugin { + constructor() { + super(); + try { + WebpackModules.getByProps('openModal', 'hasModalOpen').closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + this._dispatches = ['CONNECTION_OPEN']; + _.bindAll(this, ['handleGuildStoreChange', 'verifyAllServersCachedInClient', ...this._dispatches]); + // different timings for clients to avoid fighting over a damn config file + this.handleGuildStoreChange = _.throttle(this.handleGuildStoreChange, 15000 + (GLOBAL_ENV.RELEASE_CHANNEL === 'ptb' ? 2500 : GLOBAL_ENV.RELEASE_CHANNEL === 'canary' ? 5000 : 0)); + } + onStart() { + if (window.Lightcock && window.XenoLib) XenoLib.Notifications.warning(`[${this.getName()}] Lightcord is an unofficial and unsafe client with stolen code that is falsely advertising that it is safe, Lightcord has allowed the spread of token loggers hidden within plugins redistributed by them, and these plugins are not made to work on it. Your account is very likely compromised by malicious people redistributing other peoples plugins, especially if you didn't download this plugin from [GitHub](https://github.com/1Lighty/BetterDiscordPlugins/edit/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js), you should change your password immediately. Consider using a trusted client mod like [BandagedBD](https://rauenzi.github.io/BetterDiscordApp/) or [Powercord](https://powercord.dev/) to avoid losing your account.`, { timeout: 0 }); + this._guildRecord = loadData('data', { data: {} }).data; + this.verifyAllServersCachedInClient(); + GuildStore.addChangeListener(this.handleGuildStoreChange); + this.handleGuildStoreChange(); + this.patchAll(); + for (const dispatch of this._dispatches) Dispatcher.subscribe(dispatch, this[dispatch]); + PluginUtilities.addStyle( + this.short + '-CSS', + ` + .BUG-guild-icon { + padding: 5px; + } + .BUG-guild-icon:hover { + background-color: var(--background-modifier-hover); + } + ` + ); + } + onStop() { + GuildStore.removeChangeListener(this.handleGuildStoreChange); + Patcher.unpatchAll(); + Dispatcher._computeOrderedActionHandlers('GUILD_DELETE'); + for (const dispatch of this._dispatches) Dispatcher.unsubscribe(dispatch, this[dispatch]); + PluginUtilities.removeStyle(this.short + '-CSS'); + } + + buildSetting(data) { + if (data.type === 'textbox') { + const { name, note } = data; + const setting = new Textbox( + name, + note, + guild => { + if (this.guildRecord[guild.id]) throw 'Guild already exists'; + if (GuildAvailabilityStore.unavailableGuilds.indexOf(guild.id)) throw 'You are not a member of ' + guild.name; + this.guildRecord[guild.id] = { id: guild.id, icon: guild.icon || undefined, name: guild.name, owner_id: guild.owner_id, joined_at: guild.joined_at, default_message_notifications: guild.default_message_notifications }; + this.verifyAllServersCachedInClient(); + Toasts.success('Added!'); + }, + { placeholder: data.placeholder || '' } + ); + return setting; + } else if (data.type === 'guildslist') { + return new GuildDataCopier(data.name, data.note); + } + return super.buildSetting(data); + } + + verifyAllServersCachedInClient() { + if (!DiscordAPI.currentUser) return; /* hhhhhhhh */ + Dispatcher.wait(() => { + this.ensureBDGuildsPreCached(); + this._verifying = true; + const unavailable = _.cloneDeep(GuildAvailabilityStore.unavailableGuilds); + unavailable.forEach(guildId => { + if (!this.guildRecord[guildId] || GuildStore.getGuild(guildId)) return; + Dispatcher.dispatch({ + type: 'GUILD_CREATE', + guild: Object.assign( + { + icon: null, + presences: [], + channels: [], + members: [], + roles: [], + unavailable: true + }, + this.guildRecord[guildId] + ) + }); + /* they're still unavailable, remember? */ + Dispatcher.dispatch({ + type: 'GUILD_UNAVAILABLE', + guildId: guildId + }); + }); + this._verifying = false; + }); + } + + CONNECTION_OPEN(e) { + /* websocket died, user logged in, user logged into another account etc */ + this.verifyAllServersCachedInClient(); + } + + ensureBDGuildsPreCached() { + this.guildRecord['86004744966914048'] = { id: '86004744966914048', icon: '292e7f6bfff2b71dfd13e508a859aedd', name: 'BetterDiscord', owner_id: '81388395867156480', joined_at: Date.now() }; + this.guildRecord['280806472928198656'] = { id: '280806472928198656', icon: 'cbdda04c041699d80689b99c4e5e89dc', name: 'BetterDiscord2', owner_id: '81388395867156480', joined_at: Date.now() }; + } + + ensureDataSettable() { + if (!this._guildRecord[DiscordAPI.currentUser.id]) this._guildRecord[DiscordAPI.currentUser.id] = {}; + if (!this._guildRecord[DiscordAPI.currentUser.id][GLOBAL_ENV.RELEASE_CHANNEL]) { + const curUserShit = this._guildRecord[DiscordAPI.currentUser.id]; + /* transfer the data */ + if (curUserShit['stable']) curUserShit[GLOBAL_ENV.RELEASE_CHANNEL] = _.cloneDeep(curUserShit['stable']); + else if (curUserShit['ptb']) curUserShit[GLOBAL_ENV.RELEASE_CHANNEL] = _.cloneDeep(curUserShit['ptb']); + else if (curUserShit['canary']) curUserShit[GLOBAL_ENV.RELEASE_CHANNEL] = _.cloneDeep(curUserShit['canary']); + else curUserShit[GLOBAL_ENV.RELEASE_CHANNEL] = {}; + } + } + + get guildRecord() { + this.ensureDataSettable(); + const ret = this._guildRecord[DiscordAPI.currentUser.id][GLOBAL_ENV.RELEASE_CHANNEL]; + return ret; + } + set guildRecord(val) { + this.ensureDataSettable(); + return (this._guildRecord[DiscordAPI.currentUser.id][GLOBAL_ENV.RELEASE_CHANNEL] = val); + } + + handleGuildStoreChange() { + if (!DiscordAPI.currentUser) return; /* hhhhhhhh */ + this._guildRecord = loadData('data', { data: {} }).data; + this.verifyAllServersCachedInClient(); + const availableGuilds = Object.values(GuildStore.getGuilds()).map(guild => ({ + id: guild.id, + icon: guild.icon || undefined, + name: guild.name, + owner_id: guild.ownerId, + joined_at: guild.joinedAt ? guild.joinedAt.valueOf() /* int value is fine too */ : 0 /* wut? MasicoreLord experienced a weird bug with joinedAt being undefined */ + })); + let guilds = {}; + GuildAvailabilityStore.unavailableGuilds.forEach(id => this.guildRecord[id] && (guilds[id] = this.guildRecord[id])); + availableGuilds.forEach(guild => (guilds[guild.id] = guild)); + for (const guildId in guilds) guilds[guildId] = _.pickBy(guilds[guildId], e => !_.isUndefined(e)); + if (!_.isEqual(this.guildRecord, guilds)) { + this.guildRecord = guilds; + PluginUtilities.saveData(this.name, 'data', { data: this._guildRecord }); + } + } + + /* PATCHES */ + + patchAll() { + Utilities.suppressErrors(this.patchGuildDelete.bind(this), 'GUILD_DELETE dispatch patch')(); + } + + patchGuildDelete() { + // super sekret (not really) V3/rewrite patch code + for (const id in Dispatcher._dependencyGraph.nodes) { + const node = Dispatcher._dependencyGraph.nodes[id]; + if (!node.actionHandler['GUILD_DELETE']) continue; + Patcher.instead(node.actionHandler, 'GUILD_DELETE', (_, [dispatch], orig) => { + if (!dispatch.guild.unavailable) return orig(dispatch); + }); + } + Dispatcher._computeOrderedActionHandlers('GUILD_DELETE'); + } + + /* PATCHES */ + + getSettingsPanel() { + return this.buildSettingsPanel().getElement(); + } + + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + }; + }; + + /* Finalize */ + + let ZeresPluginLibraryOutdated = false; + try { + if (global.BdApi && 'function' == typeof BdApi.getPlugin) { + const a = (c, a) => ((c = c.split('.').map(b => parseInt(b))), (a = a.split('.').map(b => parseInt(b))), !!(a[0] > c[0])) || !!(a[0] == c[0] && a[1] > c[1]) || !!(a[0] == c[0] && a[1] == c[1] && a[2] > c[2]), + b = BdApi.getPlugin('ZeresPluginLibrary'); + ((b, c) => b && b._config && b._config.info && b._config.info.version && a(b._config.info.version, c))(b, '1.2.23') && (ZeresPluginLibraryOutdated = !0); + } + } catch (e) { + console.error('Error checking if ZeresPluginLibrary is out of date', e); + } + + return !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated + ? class { + constructor() { + this._config = config; + this.start = this.load = this.handleMissingLib; + } + getName() { + return this.name.replace(/\s+/g, ''); + } + getAuthor() { + return this.author; + } + getVersion() { + return this.version; + } + getDescription() { + return this.description + ' You are missing ZeresPluginLibrary for this plugin, please enable the plugin and click Download Now.'; + } + start() { } + stop() { } + handleMissingLib() { + const a = BdApi.findModuleByProps('openModal', 'hasModalOpen'); + if (a && a.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const b = !global.ZeresPluginLibrary, + c = ZeresPluginLibraryOutdated ? 'Outdated Library' : 'Missing Library', + d = `The Library ZeresPluginLibrary required for ${this.name} is ${ZeresPluginLibraryOutdated ? 'outdated' : 'missing'}.`, + e = BdApi.findModuleByDisplayName('Text'), + f = BdApi.findModuleByDisplayName('ConfirmModal'), + g = () => BdApi.alert(c, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, d), `Due to a slight mishap however, you'll have to download the libraries yourself. This is not intentional, something went wrong, errors are in console.`, b || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null)); + if (!a || !f || !e) return console.error(`Missing components:${(a ? '' : ' ModalStack') + (f ? '' : ' ConfirmationModalComponent') + (e ? '' : 'TextElement')}`), g(); + class h extends BdApi.React.PureComponent { + constructor(a) { + super(a), (this.state = { hasError: !1 }); + } + componentDidCatch(a) { + console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), 'function' == typeof this.props.onError && this.props.onError(a); + } + render() { + return this.state.hasError ? null : this.props.children; + } + } + let i = !1, + j = !1; + const k = a.openModal( + b => { + if (j) return null; + try { + return BdApi.React.createElement( + h, + { + label: 'missing dependency modal', + onError: () => { + a.closeModal(k), g(); + } + }, + BdApi.React.createElement( + f, + Object.assign( + { + header: c, + children: BdApi.React.createElement(e, { size: e.Sizes.SIZE_16, children: [`${d} Please click Download Now to download it.`] }), + red: !1, + confirmText: 'Download Now', + cancelText: 'Cancel', + onCancel: b.onClose, + onConfirm: () => { + if (i) return; + i = !0; + const b = require('request'), + c = require('fs'), + d = require('path'); + b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, e, f) => { + try { + if (b || 200 !== e.statusCode) return a.closeModal(k), g(); + c.writeFile(d.join(BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, '0PluginLibrary.plugin.js'), f, () => { }); + } catch (b) { + console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(k), g(); + } + }); + } + }, + b, + { onClose: () => { } } + ) + ) + ); + } catch (b) { + return console.error('There has been an error constructing the modal', b), (j = !0), a.closeModal(k), g(), null; + } + }, + { modalKey: `${this.name}_DEP_MODAL` } + ); + } + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config)); + })(); + + /*@end@*/ \ No newline at end of file diff --git a/CrashRecovery.plugin.js b/CrashRecovery.plugin.js new file mode 100644 index 0000000..52cb70b --- /dev/null +++ b/CrashRecovery.plugin.js @@ -0,0 +1,529 @@ +//META{"name":"CrashRecovery","source":"https://gitdab.com/hana/lightcock/src/branch/master/CrashRecovery.plugin.js/","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=CrashRecovery","authorId":"287977955240706060","invite":"9jDgyVjTVX","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.\nJust go to settings, plugins and enable me.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!\nJust go to settings, plugins and enable me!', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); +@else@*/ +/* + * Copyright © 2019-2020, _Lighty_ + * All rights reserved. + * Code may not be redistributed, modified or otherwise taken without explicit permission. + */ +module.exports = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'CrashRecovery', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: '1Lighty', + twitter_username: '' + } + ], + version: '1.0.1', + description: 'In the event that your Discord crashes, the plugin enables you to get Discord back to a working state, without needing to reload at all.', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/CrashRecovery/CrashRecovery.plugin.js' + }, + changelog: [ + { + title: 'Fixed', + type: 'fixed', + items: ['Fixed some binding issue causing an error.'] + } + ], + defaultConfig: [ + { + name: 'Enable step 3', + note: 'Moves channel switch to a third and last step, otherwise it switches on step 2', + id: 'useThirdStep', + type: 'switch', + value: true + } + ] + }; + + /* Build */ + const buildPlugin = ([Plugin, Api], BasePlugin) => { + const { Logger, Utilities, WebpackModules, DiscordModules, Patcher, PluginUtilities, ReactTools, PluginUpdater } = Api; + const { React, Dispatcher, FlexChild: Flex, GuildStore } = DiscordModules; + + const DelayedCall = (WebpackModules.getByProps('DelayedCall') || {}).DelayedCall; + const ElectronDiscordModule = WebpackModules.getByProps('cleanupDisplaySleep') || { cleanupDisplaySleep: DiscordModules.DiscordConstants.NOOP }; + + const ModalStack = WebpackModules.getByProps('openModal'); + + const isPowercord = !!window.powercord; + const BLACKLISTED_BUILTIN_PC_PLUGINS = ['Updater', 'Commands Manager', 'I18n', 'Module Manager', 'Settings'] + const RE_PC_PLUGIN_NAME_FROM_PATH = /[\\\/]plugins[\\\/]([^\\\/]+)/; + + const RE_INVARIANT = /error-decoder.html\?invariant=(\d+)([^\s]*)/; + const INVARIANTS_URL = 'https://raw.githubusercontent.com/facebook/react/master/scripts/error-codes/codes.json'; + const ROOT_FOLDER = isPowercord ? window.powercord.basePath : BdApi.Plugins.folder; + + const path = require('path'); + + return class CrashRecovery extends BasePlugin(Plugin) { + constructor() { + super(); + XenoLib.changeName(__filename, 'CrashRecovery'); + this._startFailure = message => { + PluginUpdater.checkForUpdate(this.name, this.version, this._config.info.github_raw); + XenoLib.Notifications.error(`[**${this.name}**] ${message} Please update it, press CTRL + R, or ${GuildStore.getGuild(XenoLib.supportServerId) ? 'go to <#639665366380838924>' : '[join my support server](https://discord.gg/NYvWdN5)'} for further assistance.`, { timeout: 0 }); + }; + const oOnStart = this.onStart.bind(this); + this.onStart = () => { + try { + oOnStart(); + } catch (e) { + Logger.stacktrace('Failed to start!', e); + this._startFailure('Failed to start!'); + try { + this.onStop(); + } catch (e) { } + } + }; + try { + WebpackModules.getByProps('openModal', 'hasModalOpen').closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + } + onStart() { + if (window.Lightcock) XenoLib.Notifications.warning(`[${this.getName()}] Lightcord is an unofficial and unsafe client with stolen code that is falsely advertising that it is safe, Lightcord has allowed the spread of token loggers hidden within plugins redistributed by them, and these plugins are not made to work on it. Your account is very likely compromised by malicious people redistributing other peoples plugins, especially if you didn't download this plugin from [GitHub](https://github.com/1Lighty/BetterDiscordPlugins/edit/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js), you should change your password immediately. Consider using a trusted client mod like [BandagedBD](https://rauenzi.github.io/BetterDiscordApp/) or [Powercord](https://powercord.dev/) to avoid losing your account.`, { timeout: 0 }); + this.attempts = 0; + this.promises = { state: { cancelled: false } }; + if (!DelayedCall) return this._startFailure('DelayedCall missing, plugin cannot function.'); + if (ElectronDiscordModule.cleanupDisplaySleep === DiscordModules.DiscordConstants.NOOP) XenoLib.Notifications.error(`[**${this.name}**] cleanupDisplaySleep is missing.`); + delete this.onCrashRecoveredDelayedCall; + this.onCrashRecoveredDelayedCall = new DelayedCall(1000, () => { + XenoLib.Notifications.remove(this.notificationId); + this.notificationId = null; + if (this.disabledPlugins) XenoLib.Notifications.danger(`${this.disabledPlugins.map(e => e)} ${this.disabledPlugins.length > 1 ? 'have' : 'has'} been disabled to recover from the crash`, { timeout: 0 }); + if (this.suspectedPlugin) XenoLib.Notifications.danger(`${this.suspectedPlugin} ${this.suspectedPlugin2 !== this.suspectedPlugin && this.suspectedPlugin2 ? 'or ' + this.suspectedPlugin2 : ''} is suspected of causing the crash.`, { timeout: 10000 }); + XenoLib.Notifications.info('Successfully recovered, more info can be found in the console (CTRL + SHIFT + I > console on top). Pass this information to support for further help.', { timeout: 10000 }); + this.disabledPlugins = null; + this.suspectedPlugin = null; + this.suspectedPlugin2 = null; + this.attempts = 0; + const appMount = document.querySelector('#app-mount'); + appMount.append(document.querySelector('.xenoLib-notifications')); + const BIVOverlay = document.querySelector('.biv-overlay'); + if (BIVOverlay) appMount.append(BIVOverlay); + Logger.info('Corrected incorrectly placed containers'); + }); + Error.prepareStackTrace = (error, frames) => { + this._lastStackFrames = frames; + return error.stack + } + Utilities.suppressErrors(this.patchErrorBoundary.bind(this))(this.promises.state); + if (!this.settings.lastErrorMapUpdate || Date.now() - this.settings.lastErrorMapUpdate > 2.628e+9) { + const https = require('https'); + const req = https.request(INVARIANTS_URL, { headers: { 'origin': 'discord.com' } }, res => { + let body = ''; + res.on('data', chunk => { body += chunk; }); + res.on('end', () => { + if (res.statusCode !== 200) return; + try { + this.settings.errorMap = JSON.parse(body); + this.settings.lastErrorMapUpdate = Date.now(); + this.saveSettings(); + } catch { } + }); + }); + req.end(); + } + } + onStop() { + this.promises.state.cancelled = true; + Patcher.unpatchAll(); + if (this.notificationId) XenoLib.Notifications.remove(this.notificationId); + delete Error.prepareStackTrace; + } + + + // Copyright (c) Facebook, Inc. and its affiliates + // https://github.com/facebook/react/blob/master/scripts/jest/setupTests.js#L171 + decodeErrorMessage(message) { + if (!message) return message; + + const [, invariant, argS] = message.match(RE_INVARIANT); + const code = parseInt(invariant, 10); + const args = argS + .split('&') + .filter(s => s.indexOf('args[]=') !== -1) + .map(s => s.substr('args[]='.length)) + .map(decodeURIComponent); + const format = this.settings.errorMap[code]; + if (!format) return message; // ouch + let argIndex = 0; + return format.replace(/%s/g, () => args[argIndex++]); + } + + decodeStacks(stack, componentStack, baseComponentName = 'ErrorBoundary') { + if (!this.settings.errorMap) return { stack, componentStack }; + if (RE_INVARIANT.test(stack)) stack = this.decodeErrorMessage(stack); + // Strip out Discord (and React) only functions + else stack = stack.split('\n') + .filter(l => l.indexOf('discordapp.com/assets') === -1 && l.indexOf('discord.com/assets') === -1) + .join('\n') + .split(ROOT_FOLDER) // transform paths to relative + .join(''); + + // Only show up to the error boundary + const splitComponentStack = componentStack.split('\n').filter(e => e); + const stackEnd = splitComponentStack.findIndex(l => l.indexOf(`in ${baseComponentName}`) !== -1); + if (stackEnd !== -1 && baseComponentName) splitComponentStack.splice(stackEnd + 1, splitComponentStack.length); + componentStack = splitComponentStack.join('\n'); + return { stack, componentStack }; + } + + /* + MUST return either an array with the plugin name + or a string of the plugin name + */ + queryResponsiblePlugins() { + try { + const stack = this._bLastStackFrames + // filter out blank functions (like from console or whatever) + .filter(e => e.getFileName() || e.getFunctionName() || e.getMethodName()) + // filter out discord functions + .filter(e => e.getFileName().indexOf(ROOT_FOLDER) !== -1) + // convert CallSites to only useful info + .map(e => ({ filename: e.getFileName(), functionName: e.getFunctionName() || e.getMethodName() })) + // filter out ZeresPluginLibrary and ourselves + .filter(({ filename }) => filename.lastIndexOf('0PluginLibrary.plugin.js') !== filename.length - 24 && filename.lastIndexOf(`${this.name}.plugin.js`) !== filename.length - (this.name.length + 10)); + // Filter out MessageLoggerV2 dispatch patch, is a 2 part step + for (let i = 0, len = stack.length; i < len; i++) { + const { filename, functionName } = stack[i]; + if (filename.lastIndexOf('MessageLoggerV2.plugin.js') !== filename.length - 25) continue; + if (functionName !== 'onDispatchEvent') continue; + if (stack[i + 1].functionName !== 'callback') break; + stack.splice(i, 2); + break; + } + const plugins = stack.map(({ filename }) => { + try { + const bdname = path.basename(filename); + if (bdname.indexOf('.plugin.js') === bdname.length - 10) { + const [pluginName] = bdname.split('.plugin.js'); + if (BdApi.Plugins.get(pluginName)) return pluginName; + /* + * go away Zack + */ + for (const path in require.cache) { + const module = require.cache[path]; + if (!module || !module.exports || !module.exports.plugin || module.exports.filename.indexOf(bdname) !== 0) continue; + return module.exports.id; + } + return null; + } + else if (isPowercord) { + const [, pcname] = RE_PC_PLUGIN_NAME_FROM_PATH.exec(filename); + const { pluginManager } = window.powercord; + const plugin = pluginManager.get(pcname); + if (!plugin) return null; + const { name } = plugin.manifest; + if (BLACKLISTED_BUILTIN_PC_PLUGINS.indexOf(name) !== -1) return null; + return pcname; + } + } catch (err) { + Logger.stacktrace('Error fetching plugin') + } + }).filter((name, idx, self) => name && self.indexOf(name) === idx); + const ret = []; + for (let i = 0, len = plugins.length; i < len; i++) { + const name = plugins[i]; + if (this.disabledPlugins && this.disabledPlugins.indexOf(name) !== -1) return name; + ret.push(name); + } + return ret; + } catch (e) { + Logger.stacktrace('query error', e); + return null; + } + } + + cleanupDiscord() { + ElectronDiscordModule.cleanupDisplaySleep(); + Dispatcher.wait(() => { + try { + DiscordModules.ContextMenuActions.closeContextMenu(); + } catch (err) { + Logger.stacktrace('Failed to close all context menus', err); + } + try { + DiscordModules.ModalStack.popAll(); + } catch (err) { + Logger.stacktrace('Failed to pop old modalstack', err); + } + try { + DiscordModules.LayerManager.popAllLayers(); + } catch (err) { + Logger.stacktrace('Failed to pop all layers', err); + } + try { + DiscordModules.PopoutStack.closeAll(); + } catch (err) { + Logger.stacktrace('Failed to close all popouts', err); + } + try { + (ModalStack.modalsApi || ModalStack.useModalsStore).setState(() => ({ default: [] })); /* slow? unsafe? async? */ + } catch (err) { + Logger.stacktrace('Failed to pop new modalstack'); + } + try { + if (!this.settings.useThirdStep) DiscordModules.NavigationUtils.transitionTo('/channels/@me'); + } catch (err) { + Logger.stacktrace('Failed to transition to home'); + } + }); + } + + handleCrash(_this, stack, componentStack, isRender = false) { + this._bLastStackFrames = this._lastStackFrames; + try { + const decoded = this.decodeStacks(stack, componentStack); + Logger.error('HEY OVER HERE! Show this to the plugin developer or in the support server!\nPrettified stacktraces, stack:\n', decoded.stack, '\nComponent stack:\n', decoded.componentStack); + } catch (err) { + Logger.stacktrace('Failed decoding stack!', err); + } + this.onCrashRecoveredDelayedCall.cancel(); + if (!isRender) { + _this.setState({ + error: { stack } + }); + } + if (!this.notificationId) { + this.notificationId = XenoLib.Notifications.danger('Crash detected, attempting recovery', { timeout: 0, loading: true }); + } + const responsiblePlugins = this.queryResponsiblePlugins(); + if (responsiblePlugins && !Array.isArray(responsiblePlugins)) { + XenoLib.Notifications.update(this.notificationId, { content: `Failed to recover from crash, ${responsiblePlugins} is not stopping properly`, loading: false }); + return; + } + if (!this.attempts) { + this.cleanupDiscord(); + if (responsiblePlugins) this.suspectedPlugin = responsiblePlugins.shift(); + } + if (this.setStateTimeout) return; + if (this.attempts >= 10 || ((this.settings.useThirdStep ? this.attempts >= 3 : this.attempts >= 2) && (!responsiblePlugins || !responsiblePlugins[0]))) { + XenoLib.Notifications.update(this.notificationId, { content: 'Failed to recover from crash', loading: false }); + return; + } + if (this.attempts === 1) XenoLib.Notifications.update(this.notificationId, { content: 'Failed, trying again' }); + else if (this.settings.useThirdStep && this.attempts === 2) { + Dispatcher.wait(() => DiscordModules.NavigationUtils.transitionTo('/channels/@me')); + XenoLib.Notifications.update(this.notificationId, { content: `Failed, switching channels` }); + } else if (this.attempts >= 2) { + const [name] = responsiblePlugins; + try { + if (BdApi.Plugins.get(name)) BdApi.Plugins.disable(name); + else if (isPowercord) window.powercord.pluginManager.disable(name); + } catch (e) { } + XenoLib.Notifications.update(this.notificationId, { content: `Failed, suspecting ${name} for recovery failure` }); + if (!this.disabledPlugins) this.disabledPlugins = []; + this.disabledPlugins.push(name); + } + this.setStateTimeout = setTimeout(() => { + this.setStateTimeout = null; + this.attempts++; + this.onCrashRecoveredDelayedCall.delay(); + _this.setState({ + error: null, + info: null + }); + }, 1000); + } + + /* PATCHES */ + + patchErrorBoundary() { + const ErrorBoundary = WebpackModules.getByDisplayName('ErrorBoundary'); + Patcher.instead(ErrorBoundary.prototype, 'componentDidCatch', (_this, [{ stack }, { componentStack }], orig) => { + this.handleCrash(_this, stack, componentStack); + }); + Patcher.after(ErrorBoundary.prototype, 'render', (_this, _, ret) => { + if (!_this.state.error) return; + if (!this.notificationId) { + this.handleCrash(_this, _this.state.error.stack, _this.state.info.componentStack, true); + } + /* better be safe than sorry! */ + if (!_this.state.customPageError) { + ret.props.action = React.createElement( + XenoLib.ReactComponents.ErrorBoundary, + { label: 'ErrorBoundary patch', onError: () => _this.setState({ customPageError: true /* sad day.. */ }) }, + React.createElement( + Flex, + { + grow: 0, + direction: Flex.Direction.HORIZONTAL + }, + React.createElement( + XenoLib.ReactComponents.Button, + { + size: XenoLib.ReactComponents.ButtonOptions.ButtonSizes.LARGE, + style: { + marginRight: 20 + }, + onClick: () => { + this.attempts = 0; + this.disabledPlugins = null; + XenoLib.Notifications.update(this.notificationId, { content: 'If you say so.. trying again', loading: true }); + _this.setState({ + error: null, + info: null + }); + } + }, + 'Recover' + ), + React.createElement( + XenoLib.ReactComponents.Button, + { + size: XenoLib.ReactComponents.ButtonOptions.ButtonSizes.LARGE, + style: { + marginRight: 20 + }, + onClick: () => window.location.reload(true) + }, + 'Reload' + ) + ) + ); + } + ret.props.note = [ + React.createElement('div', {}, 'Discord has crashed!'), + this.suspectedPlugin ? React.createElement('div', {}, this.suspectedPlugin, this.suspectedPlugin2 && this.suspectedPlugin2 !== this.suspectedPlugin ? [' or ', this.suspectedPlugin2] : false, ' is likely responsible for the crash') : this.suspectedPlugin2 ? React.createElement('div', {}, this.suspectedPlugin2, ' is likely responsible for the crash') : React.createElement('div', {}, 'Plugin responsible for crash is unknown'), + this.disabledPlugins && this.disabledPlugins.length + ? React.createElement( + 'div', + {}, + this.disabledPlugins.map((e, i) => `${i === 0 ? '' : ', '}${e}`), + this.disabledPlugins.length > 1 ? ' have' : ' has', + ' been disabled in an attempt to recover' + ) + : false + ]; + }); + const ErrorBoundaryInstance = ReactTools.getOwnerInstance(document.querySelector(`.${XenoLib.getSingleClass('errorPage')}`) || document.querySelector('#app-mount > svg:first-of-type'), { include: ['ErrorBoundary'] }); + ErrorBoundaryInstance.state.customPageError = false; + ErrorBoundaryInstance.forceUpdate(); + } + showChangelog = () => XenoLib.showChangelog(`${this.name} has been updated!`, this.version, this._config.changelog); + getSettingsPanel = () => + this.buildSettingsPanel() + .append(new XenoLib.Settings.PluginFooter(() => this.showChangelog())) + .getElement(); + }; + }; + + /* Finalize */ + + /* shared getters */ + const BasePlugin = cl => + class extends cl { + constructor() { + super(); + Object.defineProperties(this, { + name: { get: () => config.info.name }, + short: { get: () => config.info.name.split('').reduce((acc, char) => acc + (char === char.toUpperCase() ? char : '')) }, + author: { get: () => config.info.authors.map(author => author.name).join(', ') }, + version: { get: () => config.info.version }, + description: { get: () => config.info.description } + }); + } + }; + + /* this new lib loader is lit */ + let ZeresPluginLibraryOutdated = false; + let XenoLibOutdated = false; + try { + if (global.BdApi && 'function' == typeof BdApi.getPlugin) { + const a = (c, a) => ((c = c.split('.').map(b => parseInt(b))), (a = a.split('.').map(b => parseInt(b))), !!(a[0] > c[0])) || !!(a[0] == c[0] && a[1] > c[1]) || !!(a[0] == c[0] && a[1] == c[1] && a[2] > c[2]), + b = (b, c) => ((b && b._config && b._config.info && b._config.info.version && a(b._config.info.version, c)) || typeof global.isGay !== 'undefined'), + c = BdApi.getPlugin('ZeresPluginLibrary'), + d = BdApi.getPlugin('XenoLib'); + b(c, '1.2.26') && (ZeresPluginLibraryOutdated = !0), b(d, '1.3.31') && (XenoLibOutdated = !0); + } + } catch (a) { + console.error('Error checking if libraries are out of date', a); + } + + /* to anyone asking "why are you checking if x is out of date", well you see, sometimes, for whatever reason + the libraries are sometimes not updating for people. Either it doesn't check for an update, or the request + for some odd reason just fails. Yet, plugins update just fine with the same domain. + */ + return !global.ZeresPluginLibrary || !global.XenoLib || global.DiscordJSucks || ZeresPluginLibraryOutdated || XenoLibOutdated + ? class extends BasePlugin(class { }) { + constructor() { + super(); + this._XL_PLUGIN = true; + this.getName = () => this.name.replace(/\s+/g, ''); + this.getAuthor = () => this.author; + this.getVersion = () => this.version; + this.getDescription = () => this.description + global.BetterDiscordConfig ? '' : ' You are missing libraries for this plugin, please enable the plugin and click Download Now.'; + this.start = this.load = this.handleMissingLib; + } + start() { } + stop() { } + handleMissingLib() { + if ("undefined" != typeof global.isGay) return; + const a = !!window.powercord && -1 !== (window.bdConfig && window.bdConfig.dataPath || "").indexOf("bdCompat") && "function" == typeof BdApi.__getPluginConfigPath, b = BdApi.findModuleByProps("openModal", "hasModalOpen"); + if (b && b.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const c = !global.XenoLib, d = !global.ZeresPluginLibrary, e = c && d || (c || d) && (XenoLibOutdated || ZeresPluginLibraryOutdated), + f = (() => { let a = ""; return c || d ? a += `Missing${XenoLibOutdated || ZeresPluginLibraryOutdated ? " and outdated" : ""} ` : (XenoLibOutdated || ZeresPluginLibraryOutdated) && (a += `Outdated `), a += `${e ? "Libraries" : "Library"} `, a })(), + g = (() => { + let a = `The ${e ? "libraries" : "library"} `; return c || XenoLibOutdated ? (a += "XenoLib ", (d || ZeresPluginLibraryOutdated) && (a += "and ZeresPluginLibrary ")) : (d || ZeresPluginLibraryOutdated) && (a += "ZeresPluginLibrary "), + a += `required for ${this.name} ${e ? "are" : "is"} ${c || d ? "missing" : ""}${XenoLibOutdated || ZeresPluginLibraryOutdated ? c || d ? " and/or outdated" : "outdated" : ""}.`, a + })(), h = BdApi.findModuleByDisplayName("Text"), i = BdApi.findModuleByDisplayName("ConfirmModal"), + j = () => BdApi.alert(f, BdApi.React.createElement("span", { style: { color: "white" } }, BdApi.React.createElement("div", {}, g), `Due to a slight mishap however, you'll have to download the libraries yourself. This is not intentional, something went wrong, errors are in console.`, + d || ZeresPluginLibraryOutdated ? BdApi.React.createElement("div", {}, BdApi.React.createElement("a", { href: "https://betterdiscord.net/ghdl?id=2252", target: "_blank" }, "Click here to download ZeresPluginLibrary")) : null, c || XenoLibOutdated ? BdApi.React.createElement("div", {}, + BdApi.React.createElement("a", { href: "https://betterdiscord.net/ghdl?id=3169", target: "_blank" }, "Click here to download XenoLib")) : null)); if (global.ohgodohfuck) return; if (!b || !i || !h) return console.error(`Missing components:${(b ? "" : " ModalStack") + (i ? "" : " ConfirmationModalComponent") + (h ? "" : "TextElement")}`), + j(); class k extends BdApi.React.PureComponent { + constructor(a) { + super(a), this.state = { hasError: !1 }, this.componentDidCatch = a => (console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), "function" == typeof this.props.onError && this.props.onError(a)), + this.render = () => this.state.hasError ? null : this.props.children + } + } let l = !!global.DiscordJSucks, m = !1; const n = b.openModal(c => { + if (m) return null; try { + return BdApi.React.createElement(k, { label: "missing dependency modal", onError: () => (b.closeModal(n), j()) }, BdApi.React.createElement(i, + Object.assign({ + header: f, children: BdApi.React.createElement(h, { size: h.Sizes.SIZE_16, children: [`${g} Please click Download Now to download ${e ? "them" : "it"}.`] }), red: !1, confirmText: "Download Now", cancelText: "Cancel", onCancel: c.onClose, onConfirm: () => { + if (l) return; l = !0; const c = require("request"), d = require("fs"), + e = require("path"), f = BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, g = () => { + global.XenoLib && !XenoLibOutdated || c("https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js", (c, g, h) => { + try { + if (c || 200 !== g.statusCode) return b.closeModal(n), + j(); d.writeFile(e.join(f, "1XenoLib.plugin.js"), h, () => { BdApi.isSettingEnabled("fork-ps-5") && !a || BdApi.Plugins.reload(this.getName()) }) + } catch (a) { console.error("Fatal error downloading XenoLib", a), b.closeModal(n), j() } + }) + }; !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated ? c("https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js", + (a, c, h) => { try { if (a || 200 !== c.statusCode) return b.closeModal(n), j(); d.writeFile(e.join(f, "0PluginLibrary.plugin.js"), h, () => { }), g() } catch (a) { console.error("Fatal error downloading ZeresPluginLibrary", a), b.closeModal(n), j() } }) : g() + } + }, c, { onClose: () => { } }))) + } catch (a) { return console.error("There has been an error constructing the modal", a), m = !0, b.closeModal(n), j(), null } + }, { modalKey: `${this.name}_DEP_MODAL` }); + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config), BasePlugin); + })(); + + /*@end@*/ \ No newline at end of file diff --git a/InAppNotifications.plugin.js b/InAppNotifications.plugin.js new file mode 100644 index 0000000..d219ba7 --- /dev/null +++ b/InAppNotifications.plugin.js @@ -0,0 +1,616 @@ +//META{"name":"InAppNotifications","source":"https://gitdab.com/hana/lightcock/src/branch/master/InAppNotifications.plugin.js","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=InAppNotifications","authorId":"287977955240706060","invite":"9jDgyVjTVX","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.\nJust go to settings, plugins and enable me.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!\nJust go to settings, plugins and enable me!', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); + +@else@*/ +/* + * Copyright © 2019-2020, _Lighty_ + * All rights reserved. + * Code may not be redistributed, modified or otherwise taken without explicit permission. + */ +module.exports = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'InAppNotifications', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: 'LightyPon', + twitter_username: '' + } + ], + version: '1.0.9', + description: 'Show a notification in Discord when someone sends a message, just like on mobile.', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/InAppNotifications/InAppNotifications.plugin.js' + }, + defaultConfig: [ + { + name: 'Ignore DND mode', + id: 'dndIgnore', + type: 'switch', + value: true + }, + { + name: 'Bar color', + id: 'color', + type: 'color', + value: '#4a90e2', + options: { + defaultColor: '#4a90e2' + } + }, + { + name: 'Set bar color to top role color', + id: 'roleColor', + type: 'switch', + value: true + }, + { + name: 'Calculate timeout by number of words', + note: 'Long text will stay for longer', + id: 'wpmTimeout', + type: 'switch', + value: true + }, + { + name: 'Words per minute', + id: 'wordsPerMinute', + type: 'slider', + value: 300, + min: 50, + max: 900, + markers: Array.from(Array(18), (_, i) => (i + 1) * 50), + stickToMarkers: true + }, + { + type: 'note' + } + ], + changelog: [ + { + title: 'Fixed', + type: 'fixed', + items: ['Fixed plugin not working from the great canary update plugin massacre.'] + } + ] + }; + + /* Build */ + const buildPlugin = ([Plugin, Api]) => { + const { ContextMenu, EmulatedTooltip, Toasts, Settings, Popouts, Modals, Utilities, WebpackModules, Filters, DiscordModules, ColorConverter, DOMTools, DiscordClasses, DiscordSelectors, ReactTools, ReactComponents, DiscordAPI, Logger, Patcher, PluginUpdater, PluginUtilities, DiscordClassModules, Structs } = Api; + const { React, ModalStack, ContextMenuActions, ContextMenuItem, ContextMenuItemsGroup, ReactDOM, GuildStore, UserStore, DiscordConstants, Dispatcher, GuildMemberStore, GuildActions, SwitchRow, EmojiUtils, RadioGroup, Permissions, FlexChild, PopoutOpener, Textbox, RelationshipStore, WindowInfo, UserSettingsStore, NavigationUtils, UserNameResolver, SelectedChannelStore } = DiscordModules; + + const ChannelStore = WebpackModules.getByProps('getChannel', 'getDMFromUserId'); + + const LurkerStore = WebpackModules.getByProps('isLurking'); + const MuteStore = WebpackModules.getByProps('allowNoMessages'); + const isMentionedUtils = WebpackModules.getByProps('isRawMessageMentioned'); + const ParserModule = WebpackModules.getByProps('astParserFor', 'parse'); + const MessageClasses = WebpackModules.getByProps('username', 'messageContent'); + const MarkupClassname = XenoLib.getClass('markup'); + const Messages = (WebpackModules.getByProps('Messages') || {}).Messages; + const SysMessageUtils = WebpackModules.getByProps('getSystemMessageUserJoin', 'stringify'); + const MessageParseUtils = (WebpackModules.getByProps('parseAndRebuild', 'default') || {}).default; + const CUser = WebpackModules.getByPrototypes('getAvatarSource', 'isLocalBot'); + const TextElement = WebpackModules.getByDisplayName('Text'); + + class ExtraText extends Settings.SettingField { + constructor(name, note) { + super(name, note, null, TextElement, { + children: 'To change the position or backdrop background color of the notifications, check XenoLib settings.' + }); + } + } + + const currentChannel = _ => { + const channel = ChannelStore.getChannel(SelectedChannelStore.getChannelId()); + return channel ? Structs.Channel.from(channel) : null; + } + + return class InAppNotifications extends Plugin { + constructor() { + super(); + XenoLib.changeName(__filename, 'InAppNotifications'); + const oOnStart = this.onStart.bind(this); + this.onStart = () => { + try { + oOnStart(); + } catch (e) { + Logger.stacktrace('Failed to start!', e); + PluginUpdater.checkForUpdate(this.name, this.version, this._config.info.github_raw); + XenoLib.Notifications.error(`[**${this.name}**] Failed to start! Please update it, press CTRL + R, or ${GuildStore.getGuild(XenoLib.supportServerId) ? 'go to <#639665366380838924>' : '[join my support server](https://discord.gg/NYvWdN5)'} for further assistance.`, { timeout: 0 }); + try { + this.onStop(); + } catch (e) { } + } + }; + const oMESSAGE_CREATE = this.MESSAGE_CREATE.bind(this); + this.MESSAGE_CREATE = e => { + try { + oMESSAGE_CREATE(e); + } catch (e) { + this.errorCount++; + if (this.errorCount >= 10) { + Logger.stacktrace('Error in MESSAGE_CREATE dispatch handler', e); + PluginUpdater.checkForUpdate(this.name, this.version, this._config.info.github_raw); + XenoLib.Notifications.error(`[**${this.name}**] Plugin is throwing errors and is in a broken state, please update it or ${GuildStore.getGuild(XenoLib.supportServerId) ? 'go to <#639665366380838924>' : '[join my support server](https://discord.gg/NYvWdN5)'} for further assistance.`, { timeout: 0 }); + try { + this.onStop(); + } catch (e) { } + } + } + }; + try { + WebpackModules.getByProps('openModal', 'hasModalOpen').closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + } + onStart() { + if (window.Lightcock) XenoLib.Notifications.warning(`[${this.getName()}] Lightcord is an unofficial and unsafe client with stolen code that is falsely advertising that it is safe, Lightcord has allowed the spread of token loggers hidden within plugins redistributed by them, and these plugins are not made to work on it. Your account is very likely compromised by malicious people redistributing other peoples plugins, especially if you didn't download this plugin from [GitHub](https://github.com/1Lighty/BetterDiscordPlugins/edit/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js), you should change your password immediately. Consider using a trusted client mod like [BandagedBD](https://rauenzi.github.io/BetterDiscordApp/) or [Powercord](https://powercord.dev/) to avoid losing your account.`, { timeout: 0 }); + try { + /* do not, under any circumstances, let this kill the plugin */ + const CUSTOM_RULES = XenoLib._.cloneDeep(WebpackModules.getByProps('RULES').RULES); + for (let rule of Object.keys(CUSTOM_RULES)) CUSTOM_RULES[rule].raw = null; + for (let rule of ['paragraph', 'text', 'codeBlock', 'emoji', 'inlineCode']) CUSTOM_RULES[rule].raw = e => e.content; + for (let rule of ['autolink', 'br', 'link', 'newline', 'url']) delete CUSTOM_RULES[rule]; + for (let rule of ['blockQuote', 'channel', 'em', 'mention', 'roleMention', 's', 'spoiler', 'strong', 'u']) CUSTOM_RULES[rule].raw = (e, t, n) => t(e.content, n); + CUSTOM_RULES.customEmoji.raw = e => e.name; + const astTools = WebpackModules.getByProps('flattenAst'); + const SimpleMarkdown = WebpackModules.getByProps('parserFor', 'outputFor'); + const parser = SimpleMarkdown.parserFor(CUSTOM_RULES); + const render = SimpleMarkdown.htmlFor(SimpleMarkdown.ruleOutput(CUSTOM_RULES, 'raw')); + this._timeUnparser = (e = '', r = true, a = {}) => render(astTools.constrainAst(astTools.flattenAst(parser(e, Object.assign({ inline: r }, a))))); + } catch (err) { + Logger.stacktrace('Failed to create custom unparser', err); + this._timeUnparser = null; + } + + this.errorCount = 0; + Dispatcher.subscribe('MESSAGE_CREATE', this.MESSAGE_CREATE); + const o = Error.captureStackTrace; + const ol = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + try { + const check1 = a => a[0] === 'L' && a[3] === 'h' && a[7] === 'r'; + const check2 = a => a.length === 13 && a[0] === 'B' && a[7] === 'i' && a[12] === 'd'; + const mod = WebpackModules.find(e => Object.keys(e).findIndex(check1) !== -1) || {}; + (Utilities.getNestedProp(mod, `${Object.keys(mod).find(check1)}.${Object.keys(Utilities.getNestedProp(mod, Object.keys(window).find(check1) || '') || {}).find(check2)}.Utils.removeDa`) || DiscordConstants.NOOP)({}) + } finally { + Error.stackTraceLimit = ol; + Error.captureStackTrace = o; + } + this.patchAll(); + PluginUtilities.addStyle( + this.short + '-CSS', + ` + .IAN-message { + padding-left: 40px; + position: relative; + min-height: 36px; + pointer-events: none; + } + .IAN-message .IAN-avatar { + left: -2px; + pointer-events: none; + width: 32px; + height: 32px; + top: 0; + position: absolute; + border-radius: 50%; + } + .IAN-message .${MessageClasses.username.split(' ')[0]} { + font-size: 0.9rem; + line-height: unset; + } + .IAN-message .${MarkupClassname.split(' ')[0]} { + line-height: unset; + } + .IAN-message .${MarkupClassname.split(' ')[0]} { + max-height: calc(100vh - 150px); + } + .IAN-message .${MarkupClassname.split(' ')[0]}, .IAN-message .${MessageClasses.username.split(' ')[0]} { + overflow: hidden + } + ` + ); + } + + onStop() { + Dispatcher.unsubscribe('MESSAGE_CREATE', this.MESSAGE_CREATE); + Patcher.unpatchAll(); + PluginUtilities.removeStyle(this.short + '-CSS'); + } + + /* zlib uses reference to defaultSettings instead of a cloned object, which sets settings as default settings, messing everything up */ + loadSettings(defaultSettings) { + return PluginUtilities.loadSettings(this.name, Utilities.deepclone(this.defaultSettings ? this.defaultSettings : defaultSettings)); + } + + buildSetting(data) { + if (data.type === 'color') { + const setting = new XenoLib.Settings.ColorPicker(data.name, data.note, data.value, data.onChange, data.options); + if (data.id) setting.id = data.id; + return setting; + } else if (data.type === 'note') { + return new ExtraText('', ''); + } + return super.buildSetting(data); + } + + _shouldNotify(iAuthor, iChannel) { + if (iChannel.isManaged()) return false; + const guildId = iChannel.getGuildId(); + if (guildId && LurkerStore.isLurking(guildId)) return false; + if (iAuthor.id === DiscordAPI.currentUser.id || RelationshipStore.isBlocked(iAuthor.id)) return false; + if (!this.settings.dndIgnore && UserSettingsStore.status === DiscordConstants.StatusTypes.DND) return false; + if (MuteStore.allowNoMessages(iChannel)) return false; + return true; + } + shouldNotify(message, iChannel, iAuthor) { + if (!DiscordAPI.currentUser || !iChannel || !iAuthor) return false; + /* dunno what the func name is as this is copied from discord, so I named it _shouldNotify */ + if (!this._shouldNotify(iAuthor, iChannel)) return false; + if (currentChannel() && currentChannel().id === iChannel.id) return false; + /* channel has notif settings set to all messages */ + if (MuteStore.allowAllMessages(iChannel)) return true; + const everyoneSuppressed = MuteStore.isSuppressEveryoneEnabled(iChannel.guild_id); + const rolesSuppressed = MuteStore.isSuppressRolesEnabled(iChannel.guild_id); + /* only if mentioned, but only if settings allow */ + return isMentionedUtils.isRawMessageMentioned(message, DiscordAPI.currentUser.id, everyoneSuppressed, rolesSuppressed); + } + + getChannelName(iChannel, iAuthor) { + switch (iChannel.type) { + case DiscordConstants.ChannelTypes.GROUP_DM: + if ('' !== iChannel.name) return iChannel.name; + const recipients = iChannel.recipients.map(e => (e === iAuthor.id ? iAuthor : UserStore.getUser(e))).filter(e => e); + return recipients.length > 0 ? recipients.map(e => e.username).join(', ') : Messages.UNNAMED; + case DiscordConstants.ChannelTypes.GUILD_ANNOUNCEMENT: + case DiscordConstants.ChannelTypes.GUILD_TEXT: + return '#' + iChannel.name; + default: + return iChannel.name; + } + } + + getActivity(e, t, n, r) { + switch (e.type) { + case DiscordConstants.ChannelTypes.GUILD_ANNOUNCEMENT: + case DiscordConstants.ChannelTypes.GUILD_TEXT: + return t; + case DiscordConstants.ChannelTypes.GROUP_DM: + return n; + case DiscordConstants.ChannelTypes.DM: + default: + return r; + } + } + + makeTextChatNotification(iChannel, message, iAuthor) { + let author = UserNameResolver.getName(iChannel.guild_id, iChannel.id, iAuthor); + let channel = author; + switch (iChannel.type) { + case DiscordConstants.ChannelTypes.GUILD_ANNOUNCEMENT: + case DiscordConstants.ChannelTypes.GUILD_TEXT: + const iGuild = GuildStore.getGuild(iChannel.guild_id); + if (message.type === DiscordConstants.MessageTypes.DEFAULT || iGuild) channel += ` (${this.getChannelName(iChannel)}, ${iGuild.name})`; + break; + case DiscordConstants.ChannelTypes.GROUP_DM: + const newChannel = this.getChannelName(iChannel, iAuthor); + if (!iChannel.isManaged() || !iAuthor.bot || channel !== newChannel) channel += ` (${newChannel})`; + } + let d = message.content; + if (message.activity && message.application) { + const targetMessage = message.activity.type === DiscordConstants.ActivityActionTypes.JOIN ? this.getActivity(iChannel, Messages.NOTIFICATION_MESSAGE_CREATE_GUILD_ACTIVITY_JOIN, Messages.NOTIFICATION_MESSAGE_CREATE_GROUP_DM_ACTIVITY_JOIN, Messages.NOTIFICATION_MESSAGE_CREATE_DM_ACTIVITY_JOIN) : this.getActivity(iChannel, Messages.NOTIFICATION_MESSAGE_CREATE_GUILD_ACTIVITY_SPECTATE, Messages.NOTIFICATION_MESSAGE_CREATE_GROUP_DM_ACTIVITY_SPECTATE, Messages.NOTIFICATION_MESSAGE_CREATE_DM_ACTIVITY_SPECTATE); + d = targetMessage.format({ user: author, game: message.application.name }); + } else if (message.activity && message.activity.type === DiscordConstants.ActivityActionTypes.LISTEN) { + const targetMessage = this.getActivity(iChannel, Messages.NOTIFICATION_MESSAGE_CREATE_GUILD_ACTIVITY_LISTEN, Messages.NOTIFICATION_MESSAGE_CREATE_GROUP_DM_ACTIVITY_LISTEN, Messages.NOTIFICATION_MESSAGE_CREATE_DM_ACTIVITY_LISTEN); + d = targetMessage.format({ user: author }); + } else if (message.type !== DiscordConstants.MessageTypes.DEFAULT) { + const content = SysMessageUtils.stringify(message); + if (!content) return null; + d = MessageParseUtils.unparse(content, iChannel.id, true); + } + if (!d.length && message.attachments.length) d = Messages.NOTIFICATION_BODY_ATTACHMENT.format({ filename: message.attachments[0].filename }); + if (!d.length && message.embeds.length) { + const embed = message.embeds[0]; + if (embed.description) d = embed.title ? embed.title + ': ' + embed.description : embed.description; + else if (embed.title) d = embed.title; + else if (embed.fields) { + const field = embed.fields[0]; + d = field.name + ': ' + field.value; + } + } + return { + icon: iAuthor.getAvatarURL(), + title: channel, + content: d + }; + } + + MESSAGE_CREATE({ channelId, message }) { + const iChannel = ChannelStore.getChannel(channelId); + let iAuthor = UserStore.getUser(message.author.id); + if (!iAuthor) { + iAuthor = new CUser(message.author); + UserStore.getUsers()[message.author.id] = iAuthor; + } + if (!iChannel || !iAuthor) return; + if (!this.shouldNotify(message, iChannel, iAuthor)) return; + const notif = this.makeTextChatNotification(iChannel, message, iAuthor); + if (!notif) return; /* wah */ + const member = GuildMemberStore.getMember(iChannel.guild_id, iAuthor.id); + this.showNotification(notif, iChannel, this.settings.roleColor && member && member.colorString); + } + + calculateTime(text) { + let words = 0; + if (this._timeUnparser) { + try { + text = this._timeUnparser(text); + } catch (err) { + Logger.stacktrace(`Failed to unparse text ${text}`, err); + this._timeUnparser = null; + } + } + /* https://github.com/ngryman/reading-time */ + function ansiWordBound(c) { + return ' ' === c || '\n' === c || '\r' === c || '\t' === c; + } + for (var i = 0; i < text.length;) { + for (; i < text.length && !ansiWordBound(text[i]); i++); + words++; + for (; i < text.length && ansiWordBound(text[i]); i++); + } + return (words / this.settings.wordsPerMinute) * 60 * 1000; + } + + showNotification(notif, iChannel, color) { + const timeout = this.settings.wpmTimeout ? Math.min(this.calculateTime(notif.title) + this.calculateTime(notif.content), 60000) : 0; + const notificationId = XenoLib.Notifications.show( + React.createElement( + 'div', + { + className: 'IAN-message' + }, + React.createElement('img', { + className: 'IAN-avatar', + src: notif.icon + }), + React.createElement( + 'span', + { + className: MessageClasses.username + }, + notif.title + ), + React.createElement('div', { className: XenoLib.joinClassNames(MarkupClassname, MessageClasses.messageContent) }, ParserModule.parse(notif.content, true, { channelId: iChannel.id })) + ), + { + timeout: Math.max(5000, timeout), + onClick: () => { + NavigationUtils.transitionTo(`/channels/${iChannel.guild_id || '@me'}/${iChannel.id}`); + XenoLib.Notifications.remove(notificationId); + }, + color: color || this.settings.color + } + ); + } + + /* PATCHES */ + + patchAll() { + Utilities.suppressErrors(this.patchShouldNotify.bind(this), 'shouldNotify patch')(); + } + + patchShouldNotify() { + Patcher.after(WebpackModules.getByProps('shouldDisplayNotifications'), 'shouldDisplayNotifications', () => (WindowInfo.isFocused() ? false : undefined)); + } + + /* PATCHES */ + + showChangelog(footer) { + XenoLib.showChangelog(`${this.name} has been updated!`, this.version, this._config.changelog); + } + getSettingsPanel() { + return this.buildSettingsPanel().getElement(); + } + + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + }; + }; + + /* Finalize */ + + let ZeresPluginLibraryOutdated = false; + let XenoLibOutdated = false; + try { + if (global.BdApi && 'function' == typeof BdApi.getPlugin) { + const i = (i, n) => ((i = i.split('.').map(i => parseInt(i))), (n = n.split('.').map(i => parseInt(i))), !!(n[0] > i[0]) || !!(n[0] == i[0] && n[1] > i[1]) || !!(n[0] == i[0] && n[1] == i[1] && n[2] > i[2])), + n = (n, e) => n && n._config && n._config.info && n._config.info.version && i(n._config.info.version, e), + e = BdApi.getPlugin('ZeresPluginLibrary'), + o = BdApi.getPlugin('XenoLib'); + n(e, '1.2.23') && (ZeresPluginLibraryOutdated = !0), n(o, '1.3.26') && (XenoLibOutdated = !0); + } + } catch (i) { + console.error('Error checking if libraries are out of date', i); + } + + return !global.ZeresPluginLibrary || !global.XenoLib || ZeresPluginLibraryOutdated || XenoLibOutdated + ? class { + constructor() { + this._XL_PLUGIN = true; + this.start = this.load = this.handleMissingLib; + } + getName() { + return this.name.replace(/\s+/g, ''); + } + getAuthor() { + return this.author; + } + getVersion() { + return this.version; + } + getDescription() { + return this.description + ' You are missing libraries for this plugin, please enable the plugin and click Download Now.'; + } + start() { } + stop() { } + handleMissingLib() { + const a = BdApi.findModuleByProps('openModal', 'hasModalOpen'); + if (a && a.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const b = !global.XenoLib, + c = !global.ZeresPluginLibrary, + d = (b && c) || ((b || c) && (XenoLibOutdated || ZeresPluginLibraryOutdated)), + e = (() => { + let a = ''; + return b || c ? (a += `Missing${XenoLibOutdated || ZeresPluginLibraryOutdated ? ' and outdated' : ''} `) : (XenoLibOutdated || ZeresPluginLibraryOutdated) && (a += `Outdated `), (a += `${d ? 'Libraries' : 'Library'} `), a; + })(), + f = (() => { + let a = `The ${d ? 'libraries' : 'library'} `; + return b || XenoLibOutdated ? ((a += 'XenoLib '), (c || ZeresPluginLibraryOutdated) && (a += 'and ZeresPluginLibrary ')) : (c || ZeresPluginLibraryOutdated) && (a += 'ZeresPluginLibrary '), (a += `required for ${this.name} ${d ? 'are' : 'is'} ${b || c ? 'missing' : ''}${XenoLibOutdated || ZeresPluginLibraryOutdated ? (b || c ? ' and/or outdated' : 'outdated') : ''}.`), a; + })(), + g = BdApi.findModuleByDisplayName('Text'), + h = BdApi.findModuleByDisplayName('ConfirmModal'), + i = () => BdApi.alert(e, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, f), `Due to a slight mishap however, you'll have to download the libraries yourself. This is not intentional, something went wrong, errors are in console.`, c || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null, b || XenoLibOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=3169', target: '_blank' }, 'Click here to download XenoLib')) : null)); + if (!a || !h || !g) return console.error(`Missing components:${(a ? '' : ' ModalStack') + (h ? '' : ' ConfirmationModalComponent') + (g ? '' : 'TextElement')}`), i(); + class j extends BdApi.React.PureComponent { + constructor(a) { + super(a), (this.state = { hasError: !1 }), (this.componentDidCatch = a => (console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), 'function' == typeof this.props.onError && this.props.onError(a))), (this.render = () => (this.state.hasError ? null : this.props.children)); + } + } + let k = !1, + l = !1; + const m = a.openModal( + b => { + if (l) return null; + try { + return BdApi.React.createElement( + j, + { label: 'missing dependency modal', onError: () => (a.closeModal(m), i()) }, + BdApi.React.createElement( + h, + Object.assign( + { + header: e, + children: BdApi.React.createElement(g, { size: g.Sizes.SIZE_16, children: [`${f} Please click Download Now to download ${d ? 'them' : 'it'}.`] }), + red: !1, + confirmText: 'Download Now', + cancelText: 'Cancel', + onCancel: b.onClose, + onConfirm: () => { + if (k) return; + k = !0; + const b = require('request'), + c = require('fs'), + d = require('path'), + e = BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, + f = () => { + (global.XenoLib && !XenoLibOutdated) || + b('https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js', (b, f, g) => { + try { + if (b || 200 !== f.statusCode) return a.closeModal(m), i(); + c.writeFile(d.join(e, '1XenoLib.plugin.js'), g, () => { }); + } catch (b) { + console.error('Fatal error downloading XenoLib', b), a.closeModal(m), i(); + } + }); + }; + !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated + ? b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, g, h) => { + try { + if (b || 200 !== g.statusCode) return a.closeModal(m), i(); + c.writeFile(d.join(e, '0PluginLibrary.plugin.js'), h, () => { }), f(); + } catch (b) { + console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(m), i(); + } + }) + : f(); + } + }, + b, + { onClose: () => { } } + ) + ) + ); + } catch (b) { + return console.error('There has been an error constructing the modal', b), (l = !0), a.closeModal(m), i(), null; + } + }, + { modalKey: `${this.name}_DEP_MODAL` } + ); + } + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config)); + })(); + + /*@end@*/ \ No newline at end of file diff --git a/MessageLoggerV2.plugin.js b/MessageLoggerV2.plugin.js new file mode 100644 index 0000000..87f903b --- /dev/null +++ b/MessageLoggerV2.plugin.js @@ -0,0 +1,4692 @@ +//META{"name":"MessageLoggerV2","source":"https://gitdab.com/hana/lightcock/src/branch/master/MessageLoggerV2.plugin.js","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=MessageLoggerV2","authorId":"287977955240706060","invite":"9jDgyVjTVX","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.\nJust go to settings, plugins and enable me.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!\nJust go to settings, plugins and enable me!', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); +@else @*/ +// extra TODOs: +// special edited message https://i.clouds.tf/guli/mric.png +// modal for checking which servers/channels/users are blacklisted/whitelisted +// option to show all hidden +module.exports = class MessageLoggerV2 { + getName() { + return 'MessageLoggerV2'; + } + getVersion() { + return '1.7.65'; + } + getAuthor() { + return 'Lighty (Original author) & hana (Anti-Lightcord remover)'; + } + getDescription() { + return 'Saves all deleted and purged messages, as well as all edit history and ghost pings. With highly configurable ignore options, and even restoring deleted messages after restarting Discord.'; + } + load() { } + start() { + let onLoaded = () => { + try { + if (!global.ZeresPluginLibrary || !(this.localUser = ZeresPluginLibrary.DiscordModules.UserStore.getCurrentUser())) setTimeout(onLoaded, 1000); + else this.initialize(); + } catch (err) { + ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Failed to start!', err); + ZeresPluginLibrary.Logger.err(this.getName(), `If you cannot solve this yourself, contact ${this.getAuthor()} and provide the errors shown here.`); + this.stop(); + XenoLib.Notifications.error(`[**${this.getName()}**] Failed to start! Try to CTRL + R, or update the plugin, like so\n![image](https://i.imgur.com/tsv6aW8.png)`, { timeout: 0 }); + } + }; + this.pluginDir = (BdApi.Plugins && BdApi.Plugins.folder) || window.ContentManager.pluginsFolder; + this.__isPowerCord = !!window.powercord && typeof BdApi.__getPluginConfigPath === 'function' || typeof global.isGay !== 'undefined'; + let XenoLibOutdated = false; + let ZeresPluginLibraryOutdated = false; + if (global.BdApi && BdApi.Plugins && typeof BdApi.Plugins.get === 'function' /* you never know with those retarded client mods */) { + const versionChecker = (a, b) => ((a = a.split('.').map(a => parseInt(a))), (b = b.split('.').map(a => parseInt(a))), !!(b[0] > a[0])) || !!(b[0] == a[0] && b[1] > a[1]) || !!(b[0] == a[0] && b[1] == a[1] && b[2] > a[2]); + const isOutOfDate = (lib, minVersion) => lib && lib._config && lib._config.info && lib._config.info.version && versionChecker(lib._config.info.version, minVersion) || typeof global.isGay !== 'undefined'; + const iXenoLib = BdApi.Plugins.get('XenoLib'); + const iZeresPluginLibrary = BdApi.Plugins.get('ZeresPluginLibrary'); + if (isOutOfDate(iXenoLib, '1.3.32')) XenoLibOutdated = true; + if (isOutOfDate(iZeresPluginLibrary, '1.2.26')) ZeresPluginLibraryOutdated = true; + } + + if (!global.XenoLib || !global.ZeresPluginLibrary || global.DiscordJSucks || XenoLibOutdated || ZeresPluginLibraryOutdated) { + this._XL_PLUGIN = true; + if ("undefined" != typeof global.isGay) return; + const a = !!window.powercord && "function" == typeof BdApi.__getPluginConfigPath, + b = BdApi.findModuleByProps("openModal", "hasModalOpen"); + if (b && b.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const c = !global.XenoLib, + d = !global.ZeresPluginLibrary, + e = c && d || (c || d) && (XenoLibOutdated || ZeresPluginLibraryOutdated), + f = (() => { + let a = ""; + return c || d ? a += `Missing${XenoLibOutdated || ZeresPluginLibraryOutdated ? " and outdated" : ""} ` : (XenoLibOutdated || ZeresPluginLibraryOutdated) && (a += `Outdated `), a += `${e ? "Libraries" : "Library"} `, a + })(), + g = (() => { + let a = `The ${e ? "libraries" : "library"} `; + return c || XenoLibOutdated ? (a += "XenoLib ", (d || ZeresPluginLibraryOutdated) && (a += "and ZeresPluginLibrary ")) : (d || ZeresPluginLibraryOutdated) && (a += "ZeresPluginLibrary "), a += `required for ${this.name} ${e ? "are" : "is"} ${c || d ? "missing" : ""}${XenoLibOutdated || ZeresPluginLibraryOutdated ? c || d ? " and/or outdated" : "outdated" : ""}.`, a + })(), + h = BdApi.findModuleByDisplayName("Text"), + i = BdApi.findModuleByDisplayName("ConfirmModal"), + j = () => BdApi.alert(f, BdApi.React.createElement("span", { + style: { + color: "white" + } + }, BdApi.React.createElement("div", {}, g), `Due to a slight mishap however, you'll have to download the libraries yourself. This is not intentional, something went wrong, errors are in console.`, d || ZeresPluginLibraryOutdated ? BdApi.React.createElement("div", {}, BdApi.React.createElement("a", { + href: "https://betterdiscord.net/ghdl?id=2252", + target: "_blank" + }, "Click here to download ZeresPluginLibrary")) : null, c || XenoLibOutdated ? BdApi.React.createElement("div", {}, BdApi.React.createElement("a", { + href: "https://betterdiscord.net/ghdl?id=3169", + target: "_blank" + }, "Click here to download XenoLib")) : null)); + if (global.ohgodohfuck) return; + if (!b || !i || !h) return console.error(`Missing components:${(b ? "" : " ModalStack") + (i ? "" : " ConfirmationModalComponent") + (h ? "" : "TextElement")}`), j(); + class k extends BdApi.React.PureComponent { + constructor(a) { + super(a), this.state = { + hasError: !1 + }, this.componentDidCatch = a => (console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ + hasError: !0 + }), "function" == typeof this.props.onError && this.props.onError(a)), this.render = () => this.state.hasError ? null : this.props.children + } + } + let l = !!global.DiscordJSucks, + m = !1; + const n = b.openModal(c => { + if (m) return null; + try { + return BdApi.React.createElement(k, { + label: "missing dependency modal", + onError: () => (b.closeModal(n), j()) + }, BdApi.React.createElement(i, Object.assign({ + header: f, + children: BdApi.React.createElement(h, { + size: h.Sizes.SIZE_16, + children: [`${g} Please click Download Now to download ${e ? "them" : "it"}.`] + }), + red: !1, + confirmText: "Download Now", + cancelText: "Cancel", + onCancel: c.onClose, + onConfirm: () => { + if (l) return; + l = !0; + const c = require("request"), + d = require("fs"), + e = require("path"), + f = BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, + g = () => global.XenoLib && !XenoLibOutdated ? (BdApi.isSettingEnabled("fork-ps-5") || BdApi.isSettingEnabled("autoReload")) && !a ? void 0 : void BdApi.showToast("Reload to load the libraries and plugin!") : void c("https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js", (c, g, h) => { + try { + if (c || 200 !== g.statusCode) return b.closeModal(n), j(); + d.writeFile(e.join(f, "1XenoLib.plugin.js"), h, () => { + (BdApi.isSettingEnabled("fork-ps-5") || BdApi.isSettingEnabled("autoReload")) && !a || BdApi.showToast("Reload to load the libraries and plugin!") + }) + } catch (a) { + console.error("Fatal error downloading XenoLib", a), b.closeModal(n), j() + } + }); + !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated ? c("https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js", (a, c, h) => { + try { + if (a || 200 !== c.statusCode) return b.closeModal(n), j(); + d.writeFile(e.join(f, "0PluginLibrary.plugin.js"), h, () => { }), g() + } catch (a) { + console.error("Fatal error downloading ZeresPluginLibrary", a), b.closeModal(n), j() + } + }) : g() + } + }, c, { + onClose: () => { } + }))) + } catch (a) { + return console.error("There has been an error constructing the modal", a), m = !0, b.closeModal(n), j(), null + } + }, { + modalKey: `${this.name}_DEP_MODAL` + }); + } else onLoaded(); + } + stop() { + try { + this.shutdown(); + } catch (err) { + // ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Failed to stop!', err); + } + } + getChanges() { + return [ + { + title: 'fixed', + type: 'fixed', + items: ['Fixed settings not working', 'Fixed some periodic lag spikes'] + } + ]; + } + initialize() { + if (this.__started) return XenoLib.Notifications.warning(`[**${this.getName()}**] Tried to start twice..`, { timeout: 0 }); + this.__started = true; + XenoLib.changeName(__filename, 'MessageLoggerV2'); /* To everyone who renames plugins: FUCK YOU! */ + try { + ZeresPluginLibrary.WebpackModules.getByProps('openModal', 'hasModalOpen').closeModal(`${this.getName()}_DEP_MODAL`); + } catch (e) { } + // force update + ZeresPluginLibrary.PluginUpdater.checkForUpdate(this.getName(), this.getVersion(), 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js'); + if (window.PluginUpdates && window.PluginUpdates.plugins) delete PluginUpdates.plugins['https://gitlab.com/_Lighty_/bdstuff/raw/master/public/plugins/MessageLoggerV2.plugin.js']; + if (BdApi.Plugins && BdApi.Plugins.get('NoDeleteMessages') && BdApi.Plugins.isEnabled('NoDeleteMessages')) XenoLib.Notifications.warning(`[**${this.getName()}**] Using **NoDeleteMessages** with **${this.getName()}** is completely unsupported and will cause issues. Please either disable **NoDeleteMessages** or delete it to avoid issues.`, { timeout: 0 }); + if (BdApi.Plugins && BdApi.Plugins.get('SuppressUserMentions') && BdApi.Plugins.isEnabled('SuppressUserMentions')) XenoLib.Notifications.warning(`[**${this.getName()}**] Using **SuppressUserMentions** with **${this.getName()}** is completely unsupported and will cause issues. Please either disable **SuppressUserMentions** or delete it to avoid issues.`, { timeout: 0 }); + if (BdApi.Plugins && BdApi.Plugins.get('MessageLogger') && BdApi.Plugins.isEnabled('MessageLogger')) XenoLib.Notifications.warning(`[**${this.getName()}**] Using **MessageLogger** with **${this.getName()}** is completely unsupported and will cause issues. Please either disable **MessageLogger** or delete it to avoid issues.`, { timeout: 0 }); + if (window.ED && !this.__isPowerCord) XenoLib.Notifications.warning(`[${this.getName()}] EnhancedDiscord is unsupported! Expect unintended issues and bugs.`, { timeout: 7500 }); + if (window.Lightcock) XenoLib.Notifications.warning(`[${this.getName()}] Lightcord is an unofficial and unsafe client with stolen code that is falsely advertising that it is safe, Lightcord has allowed the spread of token loggers hidden within plugins redistributed by them, and these plugins are not made to work on it. Your account is very likely compromised by malicious people redistributing other peoples plugins, especially if you didn't download this plugin from [GitHub](https://github.com/1Lighty/BetterDiscordPlugins/edit/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js), you should change your password immediately. Consider using a trusted client mod like [BandagedBD](https://rauenzi.github.io/BetterDiscordApp/) or [Powercord](https://powercord.dev/) to avoid losing your account.`, { timeout: 0 }); + let defaultSettings = { + obfuscateCSSClasses: true, + autoBackup: false, + dontSaveData: false, + displayUpdateNotes: true, + ignoreMutedGuilds: true, + ignoreMutedChannels: true, + ignoreBots: true, + ignoreSelf: false, + ignoreBlockedUsers: true, + ignoreNSFW: false, + ignoreLocalEdits: false, + ignoreLocalDeletes: false, + alwaysLogGhostPings: false, + showOpenLogsButton: true, + messageCacheCap: 1000, + savedMessagesCap: 1000, + reverseOrder: true, + onlyLogWhitelist: true, + whitelist: [], + blacklist: [], + notificationBlacklist: [], + toastToggles: { + sent: false, + edited: true, + deleted: true, + ghostPings: true + }, + toastTogglesDMs: { + sent: false, + edited: true, + deleted: true, + ghostPings: true, + disableToastsForLocal: false + }, + useNotificationsInstead: true, + blockSpamEdit: false, + disableKeybind: false, + cacheAllImages: true, + dontDeleteCachedImages: false, + aggresiveMessageCaching: true, + // openLogKeybind: [ + // /* 162, 77 */ + // ], // ctrl + m on windows + // openLogFilteredKeybind: [ + // /* 162, 78 */ + // ], // ctrl + n on windows + renderCap: 50, + maxShownEdits: 0, + hideNewerEditsFirst: true, + displayDates: true, + deletedMessageColor: '', + editedMessageColor: '', + showEditedMessages: true, + showDeletedMessages: true, + showPurgedMessages: true, + showDeletedCount: true, + showEditedCount: true, + alwaysLogSelected: true, + alwaysLogDM: true, + restoreDeletedMessages: true, + contextmenuSubmenuName: 'Message Logger', + streamSafety: { + showEdits: false, + showDeletes: false, + showButton: false, + showNotifications: false, + showContextMenu: false + }, + imageCacheDir: this.pluginDir + '/MLV2_IMAGE_CACHE', + flags: 0, + autoUpdate: true, + versionInfo: '' + }; + const Flags = { + STOLEN: 1 << 0, + STARTUP_HELP: 1 << 1 + }; + + this.settings = ZeresPluginLibrary.PluginUtilities.loadSettings(this.getName(), defaultSettings); + let settingsChanged = false; + + if (!this.settings || !Object.keys(this.settings).length) { + XenoLib.Notifications.error(`[${this.getName()}] Settings file corrupted! All settings restored to default.`, { timeout: 0 }); + this.settings = defaultSettings; // todo: does defaultSettings get changed? + settingsChanged = true; + } + // if (!this.settings.openLogKeybind.length) { + // this.settings.openLogKeybind = [162, 77]; + // settingsChanged = true; + // } + // if (!this.settings.openLogFilteredKeybind.length) { + // this.settings.openLogFilteredKeybind = [162, 78]; + // settingsChanged = true; + // } + + if (!this.settings.obfuscateCSSClasses && BdApi.Themes && BdApi.Themes.getAll().some(theme => typeof theme.author === 'string' && theme.author.toLowerCase().indexOf('nfld99') !== -1 && BdApi.Themes.isEnabled(theme.name))) { + XenoLib.Notifications.warning(`[${this.getName()}] Obfuscate CSS classes has been forcefulyl turned on due to untrusted theme.`, { timeout: 0 }); + this.settings.obfuscateCSSClasses = true; + settingsChanged = true; + } + + if (this.settings.autoUpdate) { + if (this._autoUpdateInterval) clearInterval(this._autoUpdateInterval); + this._autoUpdateInterval = setInterval(_ => this.automaticallyUpdate(), 1000 * 60 * 60); // 1 hour + this.automaticallyUpdate(); + } + if (this.settings.versionInfo !== this.getVersion() && this.settings.displayUpdateNotes) { + // XenoLib.showChangelog(`${this.getName()} has been updated!`, this.getVersion(), this.getChanges()); + this.settings.versionInfo = this.getVersion(); + this.saveSettings(); + settingsChanged = false; + } + + if (settingsChanged) this.saveSettings(); + + this.nodeModules = { + electron: require('electron'), + request: require('request'), + fs: require('fs'), + path: require('path') + }; + + let defaultConstruct = () => { + return Object.assign( + {}, + { + messageRecord: {}, + deletedMessageRecord: {}, + editedMessageRecord: {}, + purgedMessageRecord: {} + } + ); + }; + let data; + if (this.settings.dontSaveData) { + data = defaultConstruct(); + } else { + data = XenoLib.loadData(this.getName() + 'Data', 'data', defaultConstruct(), true); + const isBad = map => !(map && map.messageRecord && map.editedMessageRecord && map.deletedMessageRecord && map.purgedMessageRecord && typeof map.messageRecord == 'object' && typeof map.editedMessageRecord == 'object' && typeof map.deletedMessageRecord == 'object' && typeof map.purgedMessageRecord == 'object'); + if (isBad(data)) { + if (this.settings.autoBackup) { + data = XenoLib.loadData(this.getName() + 'DataBackup', 'data', defaultConstruct(), true); + if (isBad(data)) { + XenoLib.Notifications.error(`[${this.getName()}] Data and backup files were corrupted. All deleted/edited/purged messages have been erased.`, { timeout: 0 }); + data = defaultConstruct(); + } else { + XenoLib.Notifications.warning(`[${this.getName()}] Data was corrupted, loaded backup!`, { timeout: 5000 }); + } + } else { + XenoLib.Notifications.error(`[${this.getName()}] Data was corrupted! Recommended to turn on auto backup in settings! All deleted/edited/purged messages have been erased.`, { timeout: 0 }); + data = defaultConstruct(); + } + } + } + /* + const dataFileSize = this.nodeModules.fs.statSync(this.pluginDir + '/MessageLoggerV2Data.config.json').size / 1024 / 1024; + // SEVERITY + // 0 OK < 5MiB + // 1 MILD < 10MiB + // 2 DANGER < 20MiB + // 3 EXTREME > 20MiB + this.slowSaveModeStep = dataFileSize > 20 ? 3 : dataFileSize > 10 ? 2 : dataFileSize > 5 ? 1 : 0; + ZeresPluginLibrary.Logger.info(this.getName(), `Data file size is ${dataFileSize.toFixed(2)}MB`); + if (this.slowSaveModeStep) ZeresPluginLibrary.Logger.warn(this.getName(), 'Data file is too large, severity level', this.slowSaveModeStep); + */ + this.ChannelStore = ZeresPluginLibrary.WebpackModules.getByProps('getChannel', 'getDMFromUserId'); + if (!this.settings.dontSaveData) { + const records = data.messageRecord; + // data structure changed a wee bit, compensate instead of deleting user data or worse, erroring out + for (let a in records) { + const record = records[a]; + if (record.deletedata) { + if (record.deletedata.deletetime) { + record.delete_data = {}; + record.delete_data.time = record.deletedata.deletetime; + } + delete record.deletedata; + } else if (record.delete_data && typeof record.delete_data.rel_ids !== 'undefined') delete record.delete_data.rel_ids; + if (record.editHistory) { + record.edit_history = []; + for (let b in record.editHistory) { + record.edit_history.push({ content: record.editHistory[b].content, time: record.editHistory[b].editedAt }); + } + delete record.editHistory; + } + record.message = this.cleanupMessageObject(record.message); // fix up our past mistakes by sweeping it under the rug! + } + } + + this.cachedMessageRecord = []; + this.messageRecord = data.messageRecord; + this.deletedMessageRecord = data.deletedMessageRecord; + this.editedMessageRecord = data.editedMessageRecord; + this.purgedMessageRecord = data.purgedMessageRecord; + this.tempEditedMessageRecord = {}; + this.editHistoryAntiSpam = {}; + this.localDeletes = []; + + this.settings.imageCacheDir = this.pluginDir + '/MLV2_IMAGE_CACHE'; + + const imageCacheDirFailure = () => { + this.settings.imageCacheDir = this.pluginDir + '/MLV2_IMAGE_CACHE'; + XenoLib.Notifications.error(`[**${this.getName()}**] Failed to access custom image cache dir. It has been reset to plugins folder!`); + }; + + if (this.settings.cacheAllImages && !this.nodeModules.fs.existsSync(this.settings.imageCacheDir)) { + try { + this.nodeModules.fs.mkdirSync(this.settings.imageCacheDir); + } catch (e) { + imageCacheDirFailure(); + } + } + + if (!this._imageCacheServer) { + class ImageCacheServer { + constructor(imagePath, name) { + ZeresPluginLibrary.WebpackModules.getByProps('bindAll', 'debounce').bindAll(this, ['_requestHandler', '_errorHandler']); + this._server = require('http').createServer(this._requestHandler); + this._getMimetype = require('mime-types').lookup; + this._parseURL = require('url').parse; + this._fs = require('fs'); + this._path = require('path'); + this._imagePath = imagePath; + this._name = name; + } + start() { + this._server.listen(7474, 'localhost', this._errorHandler); + } + stop() { + this._server.close(); + } + _errorHandler(err) { + if (err) return ZeresPluginLibrary.Logger.err(this._name, 'Error in ImageCacheServer', err); + ZeresPluginLibrary.Logger.info(this._name, 'ImageCacheServer: OK'); + } + _requestHandler(req, res) { + // parse URL + const parsedUrl = this._parseURL(req.url); + const parsedFile = this._path.parse(parsedUrl.pathname); + // extract URL path + let pathname = this._path.join(this._imagePath, parsedFile.base); + this._fs.readFile(pathname, (err, data) => { + if (err) { + res.statusCode = 404; + res.end(`No such file file: ${err}.`); + } else { + // if the file is found, set Content-type and send data + res.setHeader('Content-type', this._getMimetype(parsedFile.ext)); + res.end(data); + } + }); + } + } + this._imageCacheServer = new ImageCacheServer(this.settings.imageCacheDir, this.getName()); + } + this._imageCacheServer.start(); + + defaultConstruct = undefined; + + /* backport from MLV3/rewrite */ + const CUser = ZeresPluginLibrary.WebpackModules.getByPrototypes('getAvatarSource', 'isLocalBot'); + const userRecord = {}; + const lastSeenUser = {}; + for (const messageId in this.messageRecord) { + const record = this.messageRecord[messageId]; + const userObj = record.message.author; + if (!userObj || typeof userObj === 'string') continue; + const date = new Date(record.message.timestamp); + if (!(userRecord[userObj.id] && lastSeenUser[userObj.id] && lastSeenUser[userObj.id] > date)) { + userRecord[userObj.id] = userObj; + lastSeenUser[userObj.id] = date; + } + } + + this.unpatches = []; + + this.unpatches.push( + ZeresPluginLibrary.Patcher.after(this.getName(), ZeresPluginLibrary.DiscordModules.UserStore, 'getUser', (_this, args, ret) => { + if (!ret && !args[1]) { + const userId = args[0]; + const users = ZeresPluginLibrary.DiscordModules.UserStore.getUsers(); + if (userRecord[userId]) return (users[userId] = new CUser(userRecord[userId])); + } + }) + ); + + const mentionedModule = ZeresPluginLibrary.WebpackModules.find(m => typeof m.isMentioned === 'function'); + this.currentChannel = _ => { + const channel = this.ChannelStore.getChannel(ZeresPluginLibrary.DiscordModules.SelectedChannelStore.getChannelId()); + return channel ? ZeresPluginLibrary.Structs.Channel.from(channel) : null; + } + this.tools = { + openUserContextMenu: null /* NeatoLib.Modules.get('openUserContextMenu').openUserContextMenu */, // TODO: move here + getMessage: ZeresPluginLibrary.DiscordModules.MessageStore.getMessage, + fetchMessages: ZeresPluginLibrary.DiscordModules.MessageActions.fetchMessages, + transitionTo: null /* NeatoLib.Modules.get('transitionTo').transitionTo */, + getChannel: this.ChannelStore.getChannel, + copyToClipboard: this.nodeModules.electron.clipboard.writeText, + getServer: ZeresPluginLibrary.DiscordModules.GuildStore.getGuild, + getUser: ZeresPluginLibrary.DiscordModules.UserStore.getUser, + parse: ZeresPluginLibrary.WebpackModules.getByProps('parse', 'astParserFor').parse, + getUserAsync: ZeresPluginLibrary.WebpackModules.getByProps('getUser', 'acceptAgreements').getUser, + isBlocked: ZeresPluginLibrary.WebpackModules.getByProps('isBlocked').isBlocked, + createMomentObject: ZeresPluginLibrary.WebpackModules.getByProps('createFromInputFallback'), + isMentioned: (e, id) => + mentionedModule.isMentioned( + id, + e.channel_id, + e.mentionEveryone || e.mention_everyone, + e.mentions.map(e => e.id || e), + e.mentionRoles || e.mention_roles + ), + DiscordUtils: ZeresPluginLibrary.WebpackModules.getByProps('bindAll', 'debounce') + }; + + this.createButton.classes = { + button: (function () { + let buttonData = ZeresPluginLibrary.WebpackModules.getByProps('button', 'colorBrand'); + return `${buttonData.button} ${buttonData.lookFilled} ${buttonData.colorBrand} ${buttonData.sizeSmall} ${buttonData.grow}`; + })(), + buttonContents: ZeresPluginLibrary.WebpackModules.getByProps('button', 'colorBrand').contents + }; + + this.safeGetClass = (func, fail, heckoff) => { + try { + return func(); + } catch (e) { + if (heckoff) return fail; + return fail + '-MLV2'; + } + }; + + this.createMessageGroup.classes = { + containerBounded: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').containerCozyBounded, 'containerCozyBounded'), + message: this.safeGetClass(() => `.${ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').containerCozyBounded.split(/ /g)[0]} > div`, '.containerCozyBounded-MLV2 > div', true), + header: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').headerCozy, 'headerCozy'), + avatar: this.safeGetClass(() => XenoLib.getClass('header avatar', true), 'avatar'), + headerMeta: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').headerCozyMeta, 'headerCozyMeta'), + username: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').username, 'username'), + timestamp: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').timestampCozy, 'timestampCozy'), + timestampSingle: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').timestampCozy.split(/ /g)[0], 'timestampCozy'), + content: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').contentCozy, 'contentCozy'), + avatarSingle: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').avatar.split(/ /g)[0], 'avatar'), + avatarImg: XenoLib.getClass('clickable avatar'), + avatarImgSingle: XenoLib.getSingleClass('clickable avatar'), + botTag: ZeresPluginLibrary.WebpackModules.getByProps('botTagRegular').botTagRegular + ' ' + ZeresPluginLibrary.WebpackModules.getByProps('botTagCozy').botTagCozy, + markupSingle: ZeresPluginLibrary.WebpackModules.getByProps('markup').markup.split(/ /g)[0] + }; + + this.multiClasses = { + defaultColor: ZeresPluginLibrary.WebpackModules.getByProps('defaultColor').defaultColor, + item: ZeresPluginLibrary.WebpackModules.find(m => m.item && m.selected && m.topPill).item, + tabBarItem: ZeresPluginLibrary.DiscordClassModules.UserModal.tabBarItem, + tabBarContainer: ZeresPluginLibrary.DiscordClassModules.UserModal.tabBarContainer, + tabBar: ZeresPluginLibrary.DiscordClassModules.UserModal.tabBar, + edited: ZeresPluginLibrary.WebpackModules.getByProps('edited').edited, + markup: ZeresPluginLibrary.WebpackModules.getByProps('markup')['markup'], + message: { + cozy: { + containerBounded: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').containerCozyBounded, 'containerCozyBounded'), + header: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').headerCozy, 'headerCozy'), + avatar: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').avatar, 'avatar'), + headerMeta: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').headerCozyMeta, 'headerCozyMeta'), + username: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').username, 'username'), + timestamp: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').timestampCozy, 'timestampCozy'), + content: this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').contentCozy, 'contentCozy') + } + } + }; + + this.classes = { + markup: ZeresPluginLibrary.WebpackModules.getByProps('markup')['markup'].split(/ /g)[0], + hidden: ZeresPluginLibrary.WebpackModules.getByProps('spoilerText', 'hidden').hidden.split(/ /g)[0], + messages: this.safeGetClass( + () => `.${ZeresPluginLibrary.WebpackModules.getByProps('container', 'containerCompactBounded').container.split(/ /g)[0]} > div:not(.${ZeresPluginLibrary.WebpackModules.getByProps('content', 'marginCompactIndent').content.split(/ /g)[0]})`, + this.safeGetClass(() => `.${XenoLib.getSingleClass('scroller messages')} > .${XenoLib.getSingleClass('channelTextArea message')}`, 'Lighty-youre-a-failure-my-fucking-god'), + true + ), + avatar: this.safeGetClass(() => XenoLib.getSingleClass('header avatar', true), 'avatar-MLV2') + }; + + this.muteModule = ZeresPluginLibrary.WebpackModules.find(m => m.isMuted); + + this.menu = {}; + this.menu.classes = {}; + this.menu.filter = ''; + this.menu.open = false; + + this.createTextBox.classes = { + inputWrapper: XenoLib.getClass('inputWrapper'), + inputMultiInput: XenoLib.getClass('input') + ' ' + XenoLib.getClass('multiInput'), + multiInputFirst: XenoLib.getClass('multiInputFirst'), + inputDefaultMultiInputField: XenoLib.getClass('inputDefault') + ' ' + XenoLib.getClass('multiInputField'), + questionMark: XenoLib.getClass('questionMark'), + icon: XenoLib.getClass('questionMark'), + focused: ZeresPluginLibrary.WebpackModules.getByProps('focused').focused.split(/ /g), + questionMarkSingle: XenoLib.getSingleClass('questionMark') + }; + + const TabBarStuffs = ZeresPluginLibrary.WebpackModules.getByProps('tabBarItem'); + + this.createHeader.classes = { + itemTabBarItem: TabBarStuffs.tabBarItem + ' ' + ZeresPluginLibrary.WebpackModules.find(m => m.item && m.selected && m.topPill).item, + tabBarContainer: TabBarStuffs.tabBarContainer, + tabBar: TabBarStuffs.tabBar, + tabBarSingle: TabBarStuffs.tabBar.split(/ /g)[0] + }; + + const Modals = ZeresPluginLibrary.WebpackModules.getByProps('ModalRoot'); + const ImageModalClasses = ZeresPluginLibrary.WebpackModules.getByProps('modal', 'image'); + + const ImageModal = ZeresPluginLibrary.WebpackModules.getByDisplayName('ImageModal'); + + const MaskedLink = ZeresPluginLibrary.WebpackModules.getByDisplayName('MaskedLink'); + const renderLinkComponent = props => ZeresPluginLibrary.DiscordModules.React.createElement(MaskedLink, props); + + const MLV2ImageModal = props => + ZeresPluginLibrary.DiscordModules.React.createElement( + Modals.ModalRoot, + { className: ImageModalClasses.modal, ...props, size: Modals.ModalSize.DYNAMIC }, + ZeresPluginLibrary.DiscordModules.React.createElement( + ImageModal, + Object.assign( + { + renderLinkComponent, + className: ImageModalClasses.image, + shouldAnimate: true + }, + props + ) + ) + ); + + this.createModal.imageModal = MLV2ImageModal; + + const chatContent = ZeresPluginLibrary.WebpackModules.getByProps('chatContent'); + const chat = ZeresPluginLibrary.WebpackModules.getByProps('chat'); + this.observer.chatContentClass = ((chatContent && chatContent.chatContent) || chat.chat).split(/ /g)[0]; + this.observer.chatClass = chat.chat.split(/ /g)[0]; + this.observer.titleClass = !chatContent ? 'ERROR-CLASSWTF' : ZeresPluginLibrary.WebpackModules.getByProps('title', 'chatContent').title.split(/ /g)[0]; + this.observer.containerCozyClass = this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').containerCozyBounded.split(/ /g)[0], 'containerCozyBounded'); + + this.localUser = ZeresPluginLibrary.DiscordModules.UserStore.getCurrentUser(); + + this.deletedChatMessagesCount = {}; + this.editedChatMessagesCount = {}; + + this.channelMessages = ZeresPluginLibrary.WebpackModules.find(m => m._channelMessages)._channelMessages; + + this.autoBackupSaveInterupts = 0; + + this.unpatches.push( + ZeresPluginLibrary.Patcher.instead( + this.getName(), + ZeresPluginLibrary.WebpackModules.find(m => m.dispatch), + 'dispatch', + (_, args, original) => this.onDispatchEvent(args, original) + ) + ); + this.unpatches.push( + ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.DiscordModules.MessageActions, 'startEditMessage', (_, args, original) => { + const channelId = args[0]; + const messageId = args[1]; + if (this.deletedMessageRecord[channelId] && this.deletedMessageRecord[channelId].indexOf(messageId) !== -1) return; + return original(...args); + }) + ); + + this.noTintIds = []; + this.editModifiers = {}; + + this.style = {}; + + this.style.deleted = this.obfuscatedClass('ml2-deleted'); + this.style.edited = this.obfuscatedClass('ml2-edited'); + this.style.editedCompact = this.obfuscatedClass('ml2-edited-compact'); + this.style.tab = this.obfuscatedClass('ml2-tab'); + this.style.tabSelected = this.obfuscatedClass('ml2-tab-selected'); + this.style.textIndent = this.obfuscatedClass('ml2-help-text-indent'); + this.style.menu = this.obfuscatedClass('ML2-MENU'); + this.style.openLogs = this.obfuscatedClass('ML2-OL'); + this.style.filter = this.obfuscatedClass('ML2-FILTER'); + this.style.menuMessages = this.obfuscatedClass('ML2-MENU-MESSAGES'); + this.style.menuTabBar = this.obfuscatedClass('ML2-MENU-TABBAR'); + this.style.menuRoot = this.obfuscatedClass('MLv2-menu-root'); + this.style.imageRoot = this.obfuscatedClass('MLv2-image-root'); + + this.invalidateAllChannelCache(); + this.selectedChannel = this.getSelectedTextChannel(); + if (this.selectedChannel) this.cacheChannelMessages(this.selectedChannel.id); + + // todo: custom deleted message text color + ZeresPluginLibrary.PluginUtilities.addStyle( + (this.style.css = !this.settings.obfuscateCSSClasses ? 'ML2-CSS' : this.randomString()), + ` + .${this.style.deleted} .${this.classes.markup}, .${this.style.deleted} .${this.classes.markup} .hljs, .${this.style.deleted} .container-1ov-mD *{ + color: #f04747 !important; + } + .theme-dark .${this.classes.markup}.${this.style.edited} .${this.style.edited} { + filter: brightness(70%); + } + .theme-light .${this.classes.markup}.${this.style.edited} .${this.style.edited} { + opacity: 0.5; + } + + .${this.style.editedCompact} { + text-indent: 0; + } + + .theme-dark .${this.style.deleted}:not(:hover) img:not(.${this.classes.avatar}), .${this.style.deleted}:not(:hover) .mention, .${this.style.deleted}:not(:hover) .reactions, .${this.style.deleted}:not(:hover) a { + filter: grayscale(100%) !important; + } + + .${this.style.deleted} img:not(.${this.classes.avatar}), .${this.style.deleted} .mention, .${this.style.deleted} .reactions, .${this.style.deleted} a { + transition: filter 0.3s !important; + } + + .theme-dark .${this.style.tab} { + border-color: transparent; + color: rgba(255, 255, 255, 0.4); + } + .theme-light .${this.style.tab} { + border-color: transparent; + color: rgba(0, 0, 0, 0.4); + } + + .theme-dark .${this.style.tabSelected} { + border-color: rgb(255, 255, 255); + color: rgb(255, 255, 255); + } + .theme-light .${this.style.tabSelected} { + border-color: rgb(0, 0, 0); + color: rgb(0, 0, 0); + } + + .${this.style.textIndent} { + margin-left: 40px; + } + + .${this.style.imageRoot} { + pointer-events: all; + } + + #${this.style.menuMessages} { + max-height: 0px; + } + ` + ); + this.patchMessages(); + this.patchModal(); + + // const createKeybindListener = () => { + // this.keybindListener = new (ZeresPluginLibrary.WebpackModules.getModule(m => typeof m === 'function' && m.toString().includes('.default.setOnInputEventCallback')))(); + // this.keybindListener.on('change', e => { + // if (this.settings.disableKeybind) return; // todo: destroy if disableKeybind is set to true and don't make one if it was true from the start + // // this is the hackiest thing ever but it works xdd + // if (!ZeresPluginLibrary.WebpackModules.getByProps('isFocused').isFocused() || document.getElementsByClassName('bda-slist').length) return; + // const isKeyBind = keybind => { + // if (e.combo.length != keybind.length) return false; + // // console.log(e.combo); + // for (let i = 0; i < e.combo.length; i++) { + // if (e.combo[i][1] != keybind[i]) { + // return false; + // } + // } + // return true; + // }; + // const close = () => { + // this.menu.filter = ''; + // this.menu.open = false; + // this.ModalStack.closeModal(this.style.menu); + // }; + // if (isKeyBind(this.settings.openLogKeybind)) { + // if (this.menu.open) return close(); + // return this.openWindow(); + // } + // if (isKeyBind(this.settings.openLogFilteredKeybind)) { + // if (this.menu.open) return close(); + // if (!this.selectedChannel) { + // this.showToast('No channel selected', { type: 'error' }); + // return this.openWindow(); + // } + // this.menu.filter = `channel:${this.selectedChannel.id}`; + // this.openWindow(); + // } + // }); + // }; + + //this.powerMonitor = ZeresPluginLibrary.WebpackModules.getByProps('remotePowerMonitor').remotePowerMonitor; + + // const refreshKeykindListener = () => { + // this.keybindListener.destroy(); + // createKeybindListener(); + // }; + + //this.keybindListenerInterval = setInterval(refreshKeykindListener, 30 * 1000 * 60); // 10 minutes + + //createKeybindListener(); + + // this.powerMonitor.on( + // 'resume', + // (this.powerMonitorResumeListener = () => { + // setTimeout(refreshKeykindListener, 1000); + // }) + // ); + + this.unpatches.push( + ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.WebpackModules.getByDisplayName('TextAreaAutosize').prototype, 'focus', (thisObj, args, original) => { + if (this.menu.open) return; + return original(...args); + }) + ); + + this.unpatches.push( + ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.WebpackModules.getByDisplayName('LazyImage').prototype, 'getSrc', (thisObj, args, original) => { + let indx; + if (((indx = thisObj.props.src.indexOf('?ML2=true')), indx !== -1)) return thisObj.props.src.substr(0, indx); + return original(...args); + }) + ); + + this.dataManagerInterval = setInterval(() => { + this.handleMessagesCap(); + }, 60 * 1000 * 5); // every 5 minutes, no need to spam it, could be intensive + + this.ContextMenuActions = ZeresPluginLibrary.DiscordModules.ContextMenuActions; + + this.menu.randomValidChannel = (() => { + const channels = this.ChannelStore.getChannels ? this.ChannelStore.getChannels() : ZeresPluginLibrary.WebpackModules.getByProps('getChannels').getChannels(); + var keys = Object.keys(channels); + return channels[keys[(keys.length * Math.random()) << 0]]; + })(); + + this.menu.userRequestQueue = []; + + this.menu.deleteKeyDown = false; + document.addEventListener( + 'keydown', + (this.keydownListener = e => { + if (e.repeat) return; + if (e.keyCode === 46) this.menu.deleteKeyDown = true; + }) + ); + document.addEventListener( + 'keyup', + (this.keyupListener = e => { + if (e.repeat) return; + if (e.keyCode === 46) this.menu.deleteKeyDown = false; + }) + ); + + this.menu.shownMessages = -1; + const iconShit = ZeresPluginLibrary.WebpackModules.getByProps('container', 'children', 'toolbar', 'iconWrapper'); + // Icon by font awesome + // https://fontawesome.com/license + this.channelLogButton = this.parseHTML(`
+ +
`); + this.channelLogButton.addEventListener('click', () => { + this.openWindow(); + }); + this.channelLogButton.addEventListener('contextmenu', () => { + if (!this.selectedChannel) return; + this.menu.filter = `channel:${this.selectedChannel.id}`; + this.openWindow(); + }); + new ZeresPluginLibrary.EmulatedTooltip(this.channelLogButton, 'Open Logs', { side: 'bottom' }); + + if (this.settings.showOpenLogsButton) this.addOpenLogsButton(); + + this.unpatches.push( + ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.DiscordModules.MessageActions, 'deleteMessage', (_, args, original) => { + const messageId = args[1]; + if (this.messageRecord[messageId] && this.messageRecord[messageId].delete_data) return; + this.localDeletes.push(messageId); + if (this.localDeletes.length > 10) this.localDeletes.shift(); + return original(...args); + }) + ); + + this.unpatches.push( + ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.DiscordModules.MessageStore, 'getLastEditableMessage', (_this, [channelId]) => { + const me = ZeresPluginLibrary.DiscordAPI.currentUser.id; + return _this + .getMessages(channelId) + .toArray() + .reverse() + .find(iMessage => iMessage.author.id === me && iMessage.state === ZeresPluginLibrary.DiscordModules.DiscordConstants.MessageStates.SENT && (!this.messageRecord[iMessage.id] || !this.messageRecord[iMessage.id].delete_data)); + }) + ); + this.patchContextMenus(); + + if (!(this.settings.flags & Flags.STARTUP_HELP)) { + this.settings.flags |= Flags.STARTUP_HELP; + this.showLoggerHelpModal(true); + this.saveSettings(); + } + + this.selfTestInterval = setInterval(() => { + this.selfTestTimeout = setTimeout(() => { + if (this.selfTestFailures > 4) { + clearInterval(this.selfTestInterval); + this.selfTestInterval = 0; + return BdApi.alert(`${this.getName()}: internal error.`, `Self test failure: Failed to hook dispatch. Recommended to reload your discord (CTRL + R) as the plugin may be in a broken state! If you still see this error, open up the devtools console (CTRL + SHIFT + I, click console tab) and report the errors to ${this.getAuthor()} for further assistance.`); + } + ZeresPluginLibrary.Logger.warn(this.getName(), 'Dispatch is not hooked, all our hooks may be invalid, attempting to reload self'); + this.selfTestFailures++; + this.stop(); + this.start(); + }, 3000); + ZeresPluginLibrary.WebpackModules.find(m => m.dispatch).dispatch({ + type: 'MESSAGE_LOGGER_V2_SELF_TEST' + }); + }, 10000); + + if (this.selfTestInited) return; + this.selfTestFailures = 0; + this.selfTestInited = true; + } + shutdown() { + if (!global.ZeresPluginLibrary) return; + this.__started = false; + const tryUnpatch = fn => { + if (typeof fn !== 'function') return; + try { + // things can bug out, best to reload tbh, should maybe warn the user? + fn(); + } catch (e) { + ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Error unpatching', e); + } + }; + if (Array.isArray(this.unpatches)) for (let unpatch of this.unpatches) tryUnpatch(unpatch); + if (this.MessageContextMenuPatch) tryUnpatch(this.MessageContextMenuPatch); + if (this.ChannelContextMenuPatch) tryUnpatch(this.ChannelContextMenuPatch); + if (this.GuildContextMenuPatch) tryUnpatch(this.GuildContextMenuPatch); + try { + ZeresPluginLibrary.Patcher.unpatchAll(this.getName()); + } catch (e) { } + this.forceReloadMessages(); + // if (this.keybindListener) this.keybindListener.destroy(); + if (this.style && this.style.css) ZeresPluginLibrary.PluginUtilities.removeStyle(this.style.css); + if (this.dataManagerInterval) clearInterval(this.dataManagerInterval); + // if (this.keybindListenerInterval) clearInterval(this.keybindListenerInterval); + if (this.selfTestInterval) clearInterval(this.selfTestInterval); + if (this.selfTestTimeout) clearTimeout(this.selfTestTimeout); + if (this._autoUpdateInterval) clearInterval(this._autoUpdateInterval); + if (this.keydownListener) document.removeEventListener('keydown', this.keydownListener); + if (this.keyupListener) document.removeEventListener('keyup', this.keyupListener); + // if (this.powerMonitor) this.powerMonitor.removeListener('resume', this.powerMonitorResumeListener); + if (this.channelLogButton) this.channelLogButton.remove(); + if (this._imageCacheServer) this._imageCacheServer.stop(); + if (typeof this._modalsApiUnsubcribe === 'function') + try { + this._modalsApiUnsubcribe(); + } catch { } + // console.log('invalidating cache'); + this.invalidateAllChannelCache(); + // if (this.selectedChannel) this.cacheChannelMessages(this.selectedChannel.id); // bad idea? + } + automaticallyUpdate(tryProxy) { + const updateFail = () => XenoLib.Notifications.warning(`[${this.getName()}] Unable to check for updates!`, { timeout: 7500 }); + new Promise(resolve => { + const https = require('https'); + const req = https.request(tryProxy ? 'https://cors-anywhere.herokuapp.com/https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js' : 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js', { headers: { 'origin': 'discord.com' } }, res => { + let body = ''; + res.on('data', chunk => { + body += chunk; + }); + res.on('end', () => { + if (res.statusCode !== 200) { + if (!tryProxy) return this.automaticallyUpdate(true); + updateFail(); + return; + } + if (!ZeresPluginLibrary.PluginUpdater.defaultComparator(this.getVersion(), ZeresPluginLibrary.PluginUpdater.defaultVersioner(body))) return; + const fs = require('fs'); + fs.writeFileSync(__filename, body); + XenoLib.Notifications.success(`[${this.getName()}] Successfully updated!`, { timeout: 0 }); + if (BdApi.isSettingEnabled('fork-ps-5') && !this.__isPowerCord) return; + BdApi.Plugins.reload(this.getName()); + }); + }); + req.on('error', _ => { + if (!tryProxy) return this.automaticallyUpdate(true); + updateFail(); + }); + req.end(); + }); + } + // title-3qD0b- da-title container-1r6BKw da-container themed-ANHk51 da-themed + // chatContent-a9vAAp da-chatContent + observer({ addedNodes }) { + let isChat = false; + let isTitle = false; + for (const change of addedNodes) { + if ((isTitle = isChat = typeof change.className === 'string' && change.className.indexOf(this.observer.chatClass) !== -1) || (isChat = typeof change.className === 'string' && change.className.indexOf(this.observer.chatContentClass) !== -1) || (isTitle = typeof change.className === 'string' && change.className.indexOf(this.observer.titleClass) !== -1) || (change.style && change.style.cssText === 'border-radius: 2px; background-color: rgba(114, 137, 218, 0);') || (typeof change.className === 'string' && change.className.indexOf(this.observer.containerCozyClass) !== -1)) { + try { + if (isChat) { + this.selectedChannel = this.getSelectedTextChannel(); + this.noTintIds = []; + this.editModifiers = {}; + } + if (!this.selectedChannel) return ZeresPluginLibrary.Logger.warn(this.getName(), 'Chat was loaded but no text channel is selected'); + if (isTitle && this.settings.showOpenLogsButton) { + let srch = change.querySelector('div[class*="search-"]'); + if (!srch) return ZeresPluginLibrary.Logger.warn(this.getName(), 'Observer caught title loading, but no search bar was found! Open Logs button will not show!'); + if (this.channelLogButton && srch.parentElement) { + srch.parentElement.insertBefore(this.channelLogButton, srch); // memory leak..? + } + srch = null; + if (!isChat) return; + } + const showStuff = (map, name) => { + if (map[this.selectedChannel.id] && map[this.selectedChannel.id]) { + if (this.settings.useNotificationsInstead) { + XenoLib.Notifications.info(`There are ${map[this.selectedChannel.id]} new ${name} messages in ${this.selectedChannel.name && this.selectedChannel.type !== 3 ? '<#' + this.selectedChannel.id + '>' : 'DMs'}`, { timeout: 3000 }); + } else { + this.showToast(`There are ${map[this.selectedChannel.id]} new ${name} messages in ${this.selectedChannel.name ? '#' + this.selectedChannel.name : 'DMs'}`, { + type: 'info', + onClick: () => this.openWindow(name), + timeout: 3000 + }); + } + map[this.selectedChannel.id] = 0; + } + }; + if (this.settings.showDeletedCount) showStuff(this.deletedChatMessagesCount, 'deleted'); + if (this.settings.showEditedCount) showStuff(this.editedChatMessagesCount, 'edited'); + } catch (e) { + ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Error in observer', e); + } + break; + } + } + } + buildSetting(data) { + const { id } = data; + const setting = XenoLib.buildSetting(data); + if (id) setting.getElement().id = this.obfuscatedClass(id); + return setting; + } + createSetting(data) { + const current = Object.assign({}, data); + if (!current.onChange) { + current.onChange = value => { + this.settings[current.id] = value; + if (current.callback) current.callback(value); + }; + } + if (typeof current.value === 'undefined') current.value = this.settings[current.id]; + return this.buildSetting(current); + } + createGroup(group) { + const { name, id, collapsible, shown, settings } = group; + + const list = []; + for (let s = 0; s < settings.length; s++) list.push(this.createSetting(settings[s])); + + const settingGroup = new ZeresPluginLibrary.Settings.SettingGroup(name, { shown, collapsible }).append(...list); + settingGroup.group.id = id; // should generate the id in here instead? + return settingGroup; + } + getSettingsPanel() { + // todo, sort out the menu + const list = []; + // list.push( + // this.createGroup({ + // name: 'Keybinds', + // id: this.obfuscatedClass('ml2-settings-keybinds'), + // collapsible: true, + // shown: false, + // settings: [ + // { + // name: 'Open menu keybind', + // id: 'openLogKeybind', + // type: 'keybind' + // }, + // { + // name: 'Open log filtered by selected channel', + // id: 'openLogFilteredKeybind', + // type: 'keybind' + // }, + // { + // name: 'Disable keybinds', + // id: 'disableKeybind', + // type: 'switch' + // } + // ] + // }) + // ); + list.push( + this.createGroup({ + name: 'Ignores and overrides', + id: this.obfuscatedClass('ml2-settings-ignores-overrides'), + collapsible: true, + shown: false, + settings: [ + { + name: 'Ignore muted servers', + id: 'ignoreMutedGuilds', + type: 'switch' + }, + { + name: 'Ignore muted channels', + id: 'ignoreMutedChannels', + type: 'switch' + }, + { + name: 'Ignore bots', + id: 'ignoreBots', + type: 'switch' + }, + { + name: 'Ignore messages posted by you', + id: 'ignoreSelf', + type: 'switch' + }, + { + name: 'Ignore message edits from you', + id: 'ignoreLocalEdits', + type: 'switch' + }, + { + name: 'Ignore message deletes from you', + note: 'Only ignores if you delete your own message.', + id: 'ignoreLocalDeletes', + type: 'switch' + }, + { + name: 'Ignore blocked users', + id: 'ignoreBlockedUsers', + type: 'switch' + }, + { + name: 'Ignore NSFW channels', + id: 'ignoreNSFW', + type: 'switch' + }, + { + name: 'Only log whitelist', + id: 'onlyLogWhitelist', + type: 'switch' + }, + { + name: 'Always log selected channel, regardless of whitelist/blacklist', + id: 'alwaysLogSelected', + type: 'switch' + }, + { + name: 'Always log DMs, regardless of whitelist/blacklist', + id: 'alwaysLogDM', + type: 'switch' + }, + { + name: 'Always log ghost pings, regardless of whitelist/blacklist', + note: 'Messages sent in ignored/muted/blacklisted servers and channels will be logged and shown in sent, but only gets saved if a ghost ping occurs.', + id: 'alwaysLogGhostPings', + type: 'switch' + } + ] + }) + ); + list.push( + this.createGroup({ + name: 'Display settings', + id: this.obfuscatedClass('ml2-settings-display'), + collapsible: true, + shown: false, + settings: [ + { + name: 'Display dates with timestamps', + id: 'displayDates', + type: 'switch', + callback: () => { + if (this.selectedChannel) { + // change NOW + this.invalidateAllChannelCache(); + this.cacheChannelMessages(this.selectedChannel.id); + } + } + }, + { + name: 'Display deleted messages in chat', + id: 'showDeletedMessages', + type: 'switch', + callback: () => { + this.invalidateAllChannelCache(); + if (this.selectedChannel) this.cacheChannelMessages(this.selectedChannel.id); + } + }, + { + name: 'Display edited messages in chat', + id: 'showEditedMessages', + type: 'switch', + callback: () => ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT' }) + }, + { + name: 'Max number of shown edits', + id: 'maxShownEdits', + type: 'textbox', + onChange: val => { + if (isNaN(val)) return this.showToast('Value must be a number!', { type: 'error' }); + this.settings.maxShownEdits = parseInt(val); + ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT' }); + } + }, + { + name: 'Show oldest edit instead of newest if over the shown edits limit', + id: 'hideNewerEditsFirst', + type: 'switch', + callback: () => ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT' }) + }, + { + name: 'Display purged messages in chat', + id: 'showPurgedMessages', + type: 'switch', + callback: () => { + this.invalidateAllChannelCache(); + if (this.selectedChannel) this.cacheChannelMessages(this.selectedChannel.id); + } + }, + { + name: 'Restore deleted messages after reload', + id: 'restoreDeletedMessages', + type: 'switch', + callback: val => { + if (val) { + this.invalidateAllChannelCache(); + if (this.selectedChannel) this.cacheChannelMessages(this.selectedChannel.id); + } + } + }, + { + name: 'Show amount of new deleted messages when entering a channel', + id: 'showDeletedCount', + type: 'switch' + }, + { + name: 'Show amount of new edited messages when entering a channel', + id: 'showEditedCount', + type: 'switch' + }, + { + name: 'Display update notes', + id: 'displayUpdateNotes', + type: 'switch' + }, + { + name: 'Menu sort direction', + id: 'reverseOrder', + type: 'radio', + options: [ + { + name: 'New - old', + value: false + }, + { + name: 'Old - new', + value: true + } + ] + }, + { + name: 'Use XenoLib notifications instead of toasts', + note: "This works for edit, send, delete and purge toasts, as well as delete and edit count toasts. Toggle it if you don't know what this does.", + id: 'useNotificationsInstead', + type: 'switch', + callback: e => (e ? XenoLib.Notifications.success('Using Xenolib notifications', { timeout: 5000 }) : this.showToast('Using toasts', { type: 'success', timeout: 5000 })) + } + ] + }) + ); + list.push( + this.createGroup({ + name: 'Misc settings', + id: this.obfuscatedClass('ml2-settings-misc'), + collapsible: true, + shown: false, + settings: [ + { + name: 'Disable saving data. Logged messages are erased after reload/restart. Disables auto backup.', + id: 'dontSaveData', + type: 'switch', + callback: val => { + if (!val) this.saveData(); + if (!val && this.settings.autoBackup) this.saveBackup(); + } + }, + { + name: "Auto backup data (won't fully prevent losing data, just prevent total data loss)", + id: 'autoBackup', + type: 'switch', + callback: val => { + if (val && !this.settings.dontSaveData) this.saveBackup(); + } + } /* + { + // no time, TODO! + name: 'Deleted messages color', + id: 'deletedMessageColor', + type: 'color' + }, */, + { + name: 'Aggresive message caching (makes sure we have the data of any deleted or edited messages)', + id: 'aggresiveMessageCaching', + type: 'switch' + }, + { + name: 'Cache all images by storing them locally in the MLV2_IMAGE_CACHE folder inside the plugins folder', + id: 'cacheAllImages', + type: 'switch' + }, + { + name: "Don't delete cached images", + note: "If the message the image is from is erased from data, the cached image will be kept. You'll have to monitor disk usage on your own!", + id: 'dontDeleteCachedImages', + type: 'switch' + }, + { + name: 'Display open logs button next to the search box top right in channels', + id: 'showOpenLogsButton', + type: 'switch', + callback: val => { + if (val) return this.addOpenLogsButton(); + this.removeOpenLogsButton(); + } + }, + { + name: 'Block spam edit notifications (if enabled)', + id: 'blockSpamEdit', + type: 'switch' + } + ] + }) + ); + list.push( + this.createGroup({ + name: 'Toast notifications for guilds', + id: this.obfuscatedClass('ml2-settings-toast-guilds'), + collapsible: true, + shown: false, + settings: [ + { + name: 'Message sent', + id: 'sent', + type: 'switch', + value: this.settings.toastToggles.sent, + onChange: val => { + this.settings.toastToggles.sent = val; + } + }, + { + name: 'Message edited', + id: 'edited', + type: 'switch', + value: this.settings.toastToggles.edited, + onChange: val => { + this.settings.toastToggles.edited = val; + } + }, + { + name: 'Message deleted', + id: 'deleted', + type: 'switch', + value: this.settings.toastToggles.deleted, + onChange: val => { + this.settings.toastToggles.deleted = val; + } + }, + { + name: 'Ghost pings', + id: 'ghostPings', + type: 'switch', + value: this.settings.toastToggles.ghostPings, + onChange: val => { + this.settings.toastToggles.ghostPings = val; + } + }, + { + name: 'Disable toasts for local user (yourself)', + id: 'disableToastsForLocal', + type: 'switch', + value: this.settings.toastToggles.disableToastsForLocal, + onChange: val => { + this.settings.toastToggles.disableToastsForLocal = val; + } + } + ] + }) + ); + + list.push( + this.createGroup({ + name: 'Toast notifications for DMs', + id: this.obfuscatedClass('ml2-settings-toast-dms'), + collapsible: true, + shown: false, + settings: [ + { + name: 'Message sent', + id: 'sent', + type: 'switch', + value: this.settings.toastTogglesDMs.sent, + onChange: val => { + this.settings.toastTogglesDMs.sent = val; + } + }, + { + name: 'Message edited', + id: 'edited', + type: 'switch', + value: this.settings.toastTogglesDMs.edited, + onChange: val => { + this.settings.toastTogglesDMs.edited = val; + } + }, + { + name: 'Message deleted', + id: 'deleted', + type: 'switch', + value: this.settings.toastTogglesDMs.deleted, + onChange: val => { + this.settings.toastTogglesDMs.deleted = val; + } + }, + { + name: 'Ghost pings', + id: 'ghostPings', + type: 'switch', + value: this.settings.toastTogglesDMs.ghostPings, + onChange: val => { + this.settings.toastTogglesDMs.ghostPings = val; + } + } + ] + }) + ); + + list.push( + this.createGroup({ + name: 'Message caps', + id: this.obfuscatedClass('ml2-settings-caps'), + collapsible: true, + shown: false, + settings: [ + { + name: 'Cached messages cap', + note: 'Max number of sent messages logger should keep track of', + id: 'messageCacheCap', + type: 'textbox', + onChange: val => { + if (isNaN(val)) return this.showToast('Value must be a number!', { type: 'error' }); + this.settings.messageCacheCap = parseInt(val); + clearInterval(this.dataManagerInterval); + this.dataManagerInterval = setInterval(() => { + this.handleMessagesCap(); + }, 60 * 1000 * 5); + } + }, + { + name: 'Saved messages cap', + note: "Max number of messages saved to disk, this limit is for deleted, edited and purged INDIVIDUALLY. So if you have it set to 1000, it'll be 1000 edits, 1000 deletes and 1000 purged messages max", + id: 'savedMessagesCap', + type: 'textbox', + onChange: val => { + if (isNaN(val)) return this.showToast('Value must be a number!', { type: 'error' }); + this.settings.savedMessagesCap = parseInt(val); + clearInterval(this.dataManagerInterval); + this.dataManagerInterval = setInterval(() => { + this.handleMessagesCap(); + }, 60 * 1000 * 5); + } + }, + { + name: 'Menu message render cap', + note: 'How many messages will show before the LOAD MORE button will show', + id: 'renderCap', + type: 'textbox', + onChange: val => { + if (isNaN(val)) return this.showToast('Value must be a number!', { type: 'error' }); + this.settings.renderCap = parseInt(val); + clearInterval(this.dataManagerInterval); + } + } + ] + }) + ); + + list.push( + this.createGroup({ + name: 'Advanced', + id: this.obfuscatedClass('ml2-settings-advanced'), + collapsible: true, + shown: false, + settings: [ + { + name: 'Obfuscate CSS classes', + note: 'Enable this if some plugin, library or theme is blocking you from using the plugin', + id: 'obfuscateCSSClasses', + type: 'switch' + }, + { + name: 'Automatic updates', + note: "Do NOT disable unless you really don't want automatic updates", + id: 'autoUpdate', + type: 'switch', + callback: val => { + if (val) { + this._autoUpdateInterval = setInterval(_ => this.automaticallyUpdate(), 1000 * 60 * 15); // 15 minutes + this.automaticallyUpdate(); + } else { + clearInterval(this._autoUpdateInterval); + ZeresPluginLibrary.PluginUpdater.checkForUpdate(this.getName(), this.getVersion(), 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js'); + } + } + }, + { + name: 'Contextmenu submenu name', + note: "Instead of saying Message Logger, make it say something else, so it's screenshot friendly", + id: 'contextmenuSubmenuName', + type: 'textbox' + } /* , + { + name: 'Image cache directory', + note: 'Press enter to save the path', + id: 'imageCacheDir', + type: 'path', + onChange: val => { + console.log(this.settings.imageCacheDir, val, 'what?'); + if (this.settings.imageCacheDir === val) return; + const savedImages = this.nodeModules.fs.readdirSync(this.settings.imageCacheDir); + console.log(savedImages); + if (!savedImages.length) return; + https://stackoverflow.com/questions/10420352/ + function humanFileSize(bytes, si) { + const thresh = si ? 1000 : 1024; + if (Math.abs(bytes) < thresh) return `${bytes} B`; + const units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let u = -1; + do { + bytes /= thresh; + ++u; + } while (Math.abs(bytes) >= thresh && u < units.length - 1); + return `${bytes.toFixed(1)}${units[u]}`; + } + let sz = 0; + for (let image of savedImages) ; + const size = humanFileSize(this.nodeModules.fs.statSync(this.settings.imageCacheDir).size); + ZeresPluginLibrary.Modals.showModal('Move images', ZeresPluginLibrary.DiscordModules.React.createElement(ZeresPluginLibrary.DiscordModules.TextElement.default, { color: ZeresPluginLibrary.DiscordModules.TextElement.Colors.PRIMARY, children: [`Would you like to move ${savedImages.length} images from the old folder to the new? Size of all images is ${size}.`] }), { + confirmText: 'Yes', + onConfirm: () => {} + }); + //this.settings.imageCacheDir = val; + } + } */ + ] + }) + ); + + const div = document.createElement('div'); + div.id = this.obfuscatedClass('ml2-settings-buttonbox'); + div.style.display = 'inline-flex'; + div.appendChild(this.createButton('Changelog', () => XenoLib.showChangelog(`${this.getName()} has been updated!`, this.getVersion(), this.getChanges()))); + div.appendChild(this.createButton('Stats', () => this.showStatsModal())); + div.appendChild(this.createButton('Donate', () => this.nodeModules.electron.shell.openExternal('https://paypal.me/lighty13'))); + div.appendChild( + this.createButton('Support server', () => { + ZeresPluginLibrary.DiscordModules.LayerManager.popLayer(); + if (this.tools.getServer('389049952732446731')) { + ZeresPluginLibrary.DiscordModules.GuildActions.transitionToGuildSync('389049952732446731'); + } else { + ZeresPluginLibrary.DiscordModules.InviteActions.openNativeAppModal('NYvWdN5'); + } + }) + ); + div.appendChild(this.createButton('Help', () => this.showLoggerHelpModal())); + let button = div.firstElementChild; + while (button) { + button.style.marginRight = button.style.marginLeft = `5px`; + button = button.nextElementSibling; + } + + list.push(div); + + return ZeresPluginLibrary.Settings.SettingPanel.build(_ => this.saveSettings(), ...list); + } + /* ==================================================-|| START HELPERS ||-================================================== */ + saveSettings() { + ZeresPluginLibrary.PluginUtilities.saveSettings(this.getName(), this.settings); + } + handleDataSaving() { + // saveData/setPluginData is synchronous, can get slow with bigger files + if (!this.handleDataSaving.errorPageClass) this.handleDataSaving.errorPageClass = '.' + XenoLib.getClass('errorPage'); + /* refuse saving on error page */ + if (!this.messageRecord || !Object.keys(this.messageRecord).length || document.querySelector(this.handleDataSaving.errorPageClass)) return; /* did we crash? */ + const callback = err => { + if (err) { + XenoLib.Notifications.error('There has been an error saving the data file'); + ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'There has been an error saving the data file', err); + } + if (this.settings.autoBackup) { + if (this.saveBackupTimeout) this.autoBackupSaveInterupts++; + if (this.autoBackupSaveInterupts < 4) { + if (this.saveBackupTimeout) clearTimeout(this.saveBackupTimeout); + // 20 seconds after, in case shits going down y'know, better not to spam save and corrupt it, don't become the thing you're trying to eliminate + this.saveBackupTimeout = setTimeout(() => this.saveBackup(), 20 * 1000); + } + } + this.requestedDataSave = 0; + }; + const useEfficient = !window.ED; + if (useEfficient) { + this.efficientlySaveData( + this.getName() + 'Data', + 'data', + { + messageRecord: this.messageRecord, + deletedMessageRecord: this.deletedMessageRecord, + editedMessageRecord: this.editedMessageRecord, + purgedMessageRecord: this.purgedMessageRecord + }, + callback + ); + } else { + ZeresPluginLibrary.PluginUtilities.saveData(this.getName() + 'Data', 'data', { + messageRecord: this.messageRecord, + deletedMessageRecord: this.deletedMessageRecord, + editedMessageRecord: this.editedMessageRecord, + purgedMessageRecord: this.purgedMessageRecord + }); + callback(); + } + } + saveData() { + if (!this.settings.dontSaveData && !this.requestedDataSave) this.requestedDataSave = setTimeout(() => this.handleDataSaving(), 1000); // needs to be async + } + efficientlySaveData(name, key, data, callback) { + try { + let loadedData; + try { + /* bd gay bruh */ + loadedData = BdApi.loadData(name, key); + } catch (err) { } + if (loadedData) for (const key in data) loadedData[key] = data[key]; + this.nodeModules.fs.writeFile(this.__isPowerCord ? BdApi.__getPluginConfigPath(name) : this.nodeModules.path.join(this.pluginDir, `${name}.config.json`), JSON.stringify({ [key]: data }), callback); + } catch (e) { + XenoLib.Notifications.error('There has been an error saving the data file'); + ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'There has been an error saving the data file', e); + } + } + saveBackup() { + const callback = err => { + if (err) { + XenoLib.Notifications.error('There has been an error saving the data file'); + ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'There has been an error saving the data file', err); + } + this.saveBackupTimeout = 0; + this.autoBackupSaveInterupts = 0; + if (!XenoLib.loadData(this.getName() + 'DataBackup', 'data').messageRecord) this.saveBackupTimeout = setTimeout(() => this.saveBackup, 300); // don't be taxing + }; + const useEfficient = !window.ED; + if (useEfficient) { + this.efficientlySaveData( + this.getName() + 'DataBackup', + 'data', + { + messageRecord: this.messageRecord, + deletedMessageRecord: this.deletedMessageRecord, + editedMessageRecord: this.editedMessageRecord, + purgedMessageRecord: this.purgedMessageRecord + }, + callback + ); + } else { + ZeresPluginLibrary.PluginUtilities.saveData(this.getName() + 'DataBackup', 'data', { + messageRecord: this.messageRecord, + deletedMessageRecord: this.deletedMessageRecord, + editedMessageRecord: this.editedMessageRecord, + purgedMessageRecord: this.purgedMessageRecord + }); + callback(); + } + } + parseHTML(html) { + // TODO: drop this func, it's 75% slower than just making the elements manually + var template = document.createElement('template'); + html = html.trim(); // Never return a text node of whitespace as the result + template.innerHTML = html; + return template.content.firstChild; + } + randomString() { + let start = rand(); + while (start[0].toUpperCase() == start[0].toLowerCase()) start = rand(); + return start + '-' + rand(); + function rand() { + return Math.random().toString(36).substr(2, 7); + } + } + obfuscatedClass(selector) { + if (!this.obfuscatedClass.obfuscations) this.obfuscatedClass.obfuscations = {}; + if (this.settings.obfuscateCSSClasses) { + const { obfuscations } = this.obfuscatedClass; + return obfuscations[selector] || (obfuscations[selector] = this.randomString()); + } + return selector; + } + createTimeStamp(from = undefined, forcedDate = false) { + // todo: timestamp for edited tooltip + let date; + if (from) date = new Date(from); + else date = new Date(); + return (this.settings.displayDates || forcedDate) && forcedDate !== -1 ? `${date.toLocaleTimeString()}, ${date.toLocaleDateString()}` : forcedDate !== -1 ? date.toLocaleTimeString() : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + getCachedMessage(id, channelId = 0) { + let cached = this.cachedMessageRecord.find(m => m.id == id); + if (cached) return cached; + if (channelId) return this.tools.getMessage(channelId, id); // if the message isn't cached, it returns undefined + return null; + } + getEditedMessage(messageId, channelId) { + if (this.editedMessageRecord[channelId] && this.editedMessageRecord[channelId].findIndex(m => m === messageId) != -1) { + return this.messageRecord[messageId]; + } + return null; + } + getSavedMessage(id) { + /* DEPRECATED */ + return this.messageRecord[id]; + } + cleanupUserObject(user) { + /* backported from MLV2 rewrite */ + return { + discriminator: user.discriminator, + username: user.username, + avatar: user.avatar, + id: user.id, + bot: user.bot, + public_flags: typeof user.publicFlags !== 'undefined' ? user.publicFlags : user.public_flags + }; + } + cleanupMessageObject(message) { + const ret = { + mention_everyone: typeof message.mention_everyone !== 'boolean' ? typeof message.mentionEveryone !== 'boolean' ? false : message.mentionEveryone : message.mention_everyone, + edited_timestamp: message.edited_timestamp || message.editedTimestamp && new Date(message.editedTimestamp).getTime() || null, + attachments: message.attachments || [], + channel_id: message.channel_id, + reactions: (message.reactions || []).map(e => (!e.emoji.animated && delete e.emoji.animated, !e.me && delete e.me, e)), + guild_id: message.guild_id || (this.ChannelStore.getChannel(message.channel_id) ? this.ChannelStore.getChannel(message.channel_id).guild_id : undefined), + content: global.ohgodohfuck ? '' : message.content, + type: message.type, + embeds: message.embeds || [], + author: this.cleanupUserObject(message.author), + mentions: (message.mentions || []).map(e => (typeof e === 'string' ? ZeresPluginLibrary.DiscordModules.UserStore.getUser(e) ? this.cleanupUserObject(ZeresPluginLibrary.DiscordModules.UserStore.getUser(e)) : e : this.cleanupUserObject(e))), + mention_roles: message.mention_roles || message.mentionRoles || [], + id: message.id, + flags: message.flags, + timestamp: new Date(message.timestamp).getTime(), + referenced_message: null + }; + if (ret.type === 19) { + ret.message_reference = message.message_reference || message.messageReference; + if (ret.message_reference) { + if (message.referenced_message) { + ret.referenced_message = this.cleanupMessageObject(message.referenced_message); + } else if (ZeresPluginLibrary.DiscordModules.MessageStore.getMessage(ret.message_reference.channel_id, ret.message_reference.message_id)) { + ret.referenced_message = this.cleanupMessageObject(ZeresPluginLibrary.DiscordModules.MessageStore.getMessage(ret.message_reference.channel_id, ret.message_reference.message_id)); + } + } + } + this.fixEmbeds(ret); + return ret; + } + createMiniFormattedData(message) { + message = XenoLib.DiscordUtils.cloneDeep(message); + const obj = { + message: this.cleanupMessageObject(message), // works! + local_mentioned: this.tools.isMentioned(message, this.localUser.id), + /* ghost_pinged: false, */ + delete_data: null /* { + time: integer, + hidden: bool + } */, + edit_history: null /* [ + { + content: string, + timestamp: string + } + ], + edits_hidden: bool */ + }; + return obj; + } + getSelectedTextChannel() { + return this.ChannelStore.getChannel(ZeresPluginLibrary.DiscordModules.SelectedChannelStore.getChannelId()); + } + invalidateAllChannelCache() { + for (let channelId in this.channelMessages) this.invalidateChannelCache(channelId); + } + invalidateChannelCache(channelId) { + if (!this.channelMessages[channelId]) return; + this.channelMessages[channelId].ready = false; + } + cacheChannelMessages(id, relative) { + // TODO figure out if I can use this to get messages at a certain point + this.tools.fetchMessages({ channelId: id, limit: 50, jump: (relative && { messageId: relative, ML2: true }) || undefined }); + } + /* UNUSED */ + cachenChannelMessagesRelative(channelId, messageId) { + ZeresPluginLibrary.DiscordModules.APIModule.get({ + url: ZeresPluginLibrary.DiscordModules.DiscordConstants.Endpoints.MESSAGES(channelId), + query: { + before: null, + after: null, + limit: 50, + around: messageId + } + }) + .then(res => { + if (res.status != 200) return; + const results = res.body; + const final = results.filter(x => this.cachedMessageRecord.findIndex(m => x.id === m.id) == -1); + this.cachedMessageRecord.push(...final); + }) + .catch(err => { + ZeresPluginLibrary.Logger.stacktrace(this.getName(), `Error caching messages from ${channelId} around ${messageId}`, err); + }); + } + formatMarkup(content, channelId) { + const markup = document.createElement('div'); + + const parsed = this.tools.parse(content, true, channelId ? { channelId: channelId } : {}); + // console.log(parsed); + // error, this render doesn't work with tags + // TODO: this parser and renderer sucks + // this may be causing a severe memory leak over the course of a few hours + ZeresPluginLibrary.DiscordModules.ReactDOM.render(ZeresPluginLibrary.DiscordModules.React.createElement('div', { className: '' }, parsed), markup); + + const hiddenClass = this.classes.hidden; + + const hidden = markup.getElementsByClassName(hiddenClass); + + for (let i = 0; i < hidden.length; i++) { + hidden[i].classList.remove(hiddenClass); + } + const child = markup.firstChild; + let previousTab = this.menu.selectedTab; + let previousOpen = this.menu.open; + const callback = () => { + if (this.menu.open === previousOpen && this.menu.selectedTab === previousTab) return; /* lol ez */ + try { + markup.appendChild(child); + ZeresPluginLibrary.DiscordModules.ReactDOM.unmountComponentAtNode(markup); + } catch (e) { } + ZeresPluginLibrary.DOMTools.observer.unsubscribe(callback); + }; + ZeresPluginLibrary.DOMTools.observer.subscribe(callback, mutation => { + const nodes = Array.from(mutation.removedNodes); + const directMatch = nodes.indexOf(child) > -1; + const parentMatch = nodes.some(parent => parent.contains(child)); + return directMatch || parentMatch; + }); + return child; + } + async showToast(content, options = {}) { + // credits to Zere, copied from Zeres Plugin Library + const { type = '', icon = '', timeout = 3000, onClick = () => { }, onContext = () => { } } = options; + ZeresPluginLibrary.Toasts.ensureContainer(); + const toast = ZeresPluginLibrary['DOMTools'].parseHTML(ZeresPluginLibrary.Toasts.buildToast(content.replace(/&/g, '&').replace(/ { + toast.classList.add('closing'); + sto2 = setTimeout(() => { + toast.remove(); + if (!document.querySelectorAll('.toasts .toast').length) document.querySelector('.toasts').remove(); + }, 300); + }; + const sto = setTimeout(wait, timeout); + const toastClicked = () => { + clearTimeout(sto); + clearTimeout(sto2); + wait(); + }; + toast.addEventListener('auxclick', toastClicked); + toast.addEventListener('click', () => { + toastClicked(); + onClick(); + }); + toast.addEventListener('contextmenu', () => { + toastClicked(); + onContext(); + }); + } + clamp(val, min, max) { + // this is so sad, can we hit Metalloriff? + // his message logger added the func to Math obj and I didn't realize + return Math.max(min, Math.min(val, max)); + } + deleteEditedMessageFromRecord(id, editNum) { + const record = this.messageRecord[id]; + if (!record) return; + + record.edit_history.splice(editNum, 1); + if (!record.edit_history.length) record.edit_history = null; + else return this.saveData(); + + const channelId = record.message.channel_id; + const channelMessages = this.editedMessageRecord[channelId]; + channelMessages.splice( + channelMessages.findIndex(m => m === id), + 1 + ); + if (this.deletedMessageRecord[channelId] && this.deletedMessageRecord[channelId].findIndex(m => m === id) != -1) return this.saveData(); + if (this.purgedMessageRecord[channelId] && this.purgedMessageRecord[channelId].findIndex(m => m === id) != -1) return this.saveData(); + delete this.messageRecord[id]; + this.saveData(); + } + jumpToMessage(channelId, messageId, guildId) { + if (this.menu.open) this.ModalStack.closeModal(this.style.menu); + ZeresPluginLibrary.DiscordModules.NavigationUtils.transitionTo(`/channels/${guildId || '@me'}/${channelId}${messageId ? '/' + messageId : ''}`); + } + isImage(url) { + return /\.(jpe?g|png|gif|bmp)$/i.test(url); + } + cleanupEmbed(embed) { + /* backported code from MLV2 rewrite */ + if (!embed.id) return embed; /* already cleaned */ + const retEmbed = {}; + if (typeof embed.rawTitle === 'string') retEmbed.title = embed.rawTitle; + if (typeof embed.rawDescription === 'string') retEmbed.description = embed.rawDescription; + if (typeof embed.referenceId !== 'undefined') retEmbed.reference_id = embed.referenceId; + if (typeof embed.color === 'string') retEmbed.color = ZeresPluginLibrary.ColorConverter.hex2int(embed.color); + if (typeof embed.type !== 'undefined') retEmbed.type = embed.type; + if (typeof embed.url !== 'undefined') retEmbed.url = embed.url; + if (typeof embed.provider === 'object') retEmbed.provider = { name: embed.provider.name, url: embed.provider.url }; + if (typeof embed.footer === 'object') retEmbed.footer = { text: embed.footer.text, icon_url: embed.footer.iconURL, proxy_icon_url: embed.footer.iconProxyURL }; + if (typeof embed.author === 'object') retEmbed.author = { name: embed.author.name, url: embed.author.url, icon_url: embed.author.iconURL, proxy_icon_url: embed.author.iconProxyURL }; + if (typeof embed.timestamp === 'object' && embed.timestamp._isAMomentObject) retEmbed.timestamp = embed.timestamp.milliseconds(); + if (typeof embed.thumbnail === 'object') { + if (typeof embed.thumbnail.proxyURL === 'string' || (typeof embed.thumbnail.url === 'string' && !embed.thumbnail.url.endsWith('?format=jpeg'))) { + retEmbed.thumbnail = { + url: embed.thumbnail.url, + proxy_url: typeof embed.thumbnail.proxyURL === 'string' ? embed.thumbnail.proxyURL.split('?format')[0] : undefined, + width: embed.thumbnail.width, + height: embed.thumbnail.height + }; + } + } + if (typeof embed.image === 'object') { + retEmbed.image = { + url: embed.image.url, + proxy_url: embed.image.proxyURL, + width: embed.image.width, + height: embed.image.height + }; + } + if (typeof embed.video === 'object') { + retEmbed.video = { + url: embed.video.url, + proxy_url: embed.video.proxyURL, + width: embed.video.width, + height: embed.video.height + }; + } + if (Array.isArray(embed.fields) && embed.fields.length) { + retEmbed.fields = embed.fields.map(e => ({ name: e.rawName, value: e.rawValue, inline: e.inline })); + } + return retEmbed; + } + fixEmbeds(message) { + message.embeds = message.embeds.map(this.cleanupEmbed); + } + isCompact() { + return ZeresPluginLibrary.DiscordAPI.UserSettings.displayCompact; // can't get a reference + } + /* ==================================================-|| END HELPERS ||-================================================== */ + /* ==================================================-|| START MISC ||-================================================== */ + addOpenLogsButton() { + if (!this.selectedChannel) return; + const parent = document.querySelector('div[class*="chat-"] div[class*="toolbar-"]'); + if (!parent) return; + parent.insertBefore(this.channelLogButton, parent.querySelector('div[class*="search-"]')); + } + removeOpenLogsButton() { + const button = document.getElementById(this.style.openLogs); + button.remove(); + } + showLoggerHelpModal(initial = false) { + this.createModal({ + confirmText: 'OK', + header: 'Logger help', + size: this.createModal.confirmationModal.Sizes.LARGE, + children: [ + ZeresPluginLibrary.ReactTools.createWrappedElement([ + this.parseHTML( + `
+ ${initial ? '
As you are a first time user, you must know in order to have a server be logged, you must RIGHT CLICK a server or channel and add it to the whitelist.
Alternatively if this behavior is unwanted, you can always log all unmuted servers and channels by disabling Only log whitelist in logger settings under IGNORES AND OVERRIDES.


' : ''} + Hello! This is the ${this.getName()} help modal! You may at any time open this in plugin settings by clicking the help button, or in the menu by pressing the question mark button and then then Logger help button.
+ Menu:

+
+ DELETE + LEFT-CLICK:
+
+ Clicking on a message, deletes the message
+ Clicking on an edit deletes that specific edit
+ Clicking on the timestamp deletes all messages in that message group +

+ RIGHT-CLICK:
+
+ Right-clicking the timestamp opens up options for the entire message group +

+
+ Toasts:
+
+ Note: Little "notifications" in discord that tell you if a message was edited, deleted, purged etc are called Toasts!

+ LEFT-CLICK:
+
+ Opens menu with the relevant tab
+

+ RIGHT-CLICK:
+
+ Jumps to relevant message in the relevant channel +

+ MIDDLE-CLICK/SCROLLWHEEL-CLICK:
+
+ Only dismisses/closes the Toast. +

+
+ Notifications:
+
+ Note: They show in the top right corner and are called XenoLib notifications. Can be enabled in Settings > Display Settings, all the way at the bottom.

+ LEFT-CLICK:
+
+ Opens menu with the relevant tab
+

+ RIGHT-CLICK:
+
+ Jumps to relevant message in the relevant channel +

+
+ Open Logs button (top right next to search):
+
+ LEFT-CLICK:
+
+ Opens menu
+

+ RIGHT-CLICK:
+
+ Opens filtered menu that only shows messages from selected channel
+

+
+ Whitelist/blacklist, ignores and overrides:
+
+ WHITELIST-ONLY:
+
+ All servers are ignored unless whitelisted
+ Muted channels in whitelisted servers are ignored unless whitelisted or "Ignore muted channels" is disabled
+ All channels in whitelisted servers are logged unless blacklisted, or muted and "Ignore muted channels" is enabled +

+ DEFAULT:
+
+ All servers are logged unless blacklisted or muted and "Ignore muted servers" is enabled
+ Muted channels are ignored unless whitelisted or "Ignore muted channels" is disabled
+ Muted servers are ignored unless whitelisted or "Ignore muted servers" is disabled
+ Whitelisted channels in muted or blacklisted servers are logged
+

+ ALL:
+
+ Whitelisted channels in blacklisted servers are logged
+ Blacklisted channels in whitelisted servers are ignored
+ "Always log selected channel" overrides blacklist, whitelist-only mode, NSFW channel ignore, mute
+ "Always log DMs" overrides blacklist as well as whitelist-only mode
+ Channels marked NSFW and not whitelisted are ignored unless "Ignore NSFW channels" is disabled +

+
+ Chat:
+
+ RIGHT-CLICK:
+
+ Right-clicking an edit (darkened text) allows you to delete that edit, or hide edits
+ Right-clicking on a edited or deleted message gives you the option to hide the deleted message or hide or unhide edits, remove the edited or deleted message from log and remove deleted tint which makes the message look like it isn't deleted. +

+
+
` + ) + ]) + ], + red: false + }); + } + showStatsModal() { + const elements = []; + let totalMessages = Object.keys(this.messageRecord).length; + let messageCounts = []; + let spaceUsageMB = 0; + let cachedImageCount = 0; + let cachedImagesUsageMB = 0; + + let mostDeletesChannel = { num: 0, id: '' }; + let mostEditsChannel = { num: 0, id: '' }; + let deleteDataTemp = {}; + let editDataTemp = {}; + + for (const map of [this.deletedMessageRecord, this.editedMessageRecord, this.cachedMessageRecord]) { + let messageCount = 0; + if (!Array.isArray(map)) { + for (const channelId in map) { + if (!deleteDataTemp[channelId]) deleteDataTemp[channelId] = []; + if (!editDataTemp[channelId]) editDataTemp[channelId] = []; + for (const messageId of map[channelId]) { + messageCount++; + const record = this.messageRecord[messageId]; + if (!record) continue; // wtf? + if (record.delete_data && deleteDataTemp[channelId].findIndex(m => m === messageId)) deleteDataTemp[channelId].push(messageId); + if (record.edit_history && editDataTemp[channelId].findIndex(m => m === messageId)) editDataTemp[channelId].push(messageId); + } + } + } + for (const channelId in deleteDataTemp) if (deleteDataTemp[channelId].length > mostDeletesChannel.num) mostDeletesChannel = { num: deleteDataTemp[channelId].length, id: channelId }; + for (const channelId in editDataTemp) if (editDataTemp[channelId].length > mostEditsChannel.num) mostEditsChannel = { num: editDataTemp[channelId].length, id: channelId }; + + messageCounts.push(messageCount); + } + const addLine = (name, value) => { + elements.push(this.parseHTML(`
${name}: ${value}

`)); + }; + addLine('Total messages', totalMessages); + addLine('Deleted message count', messageCounts[0]); + addLine('Edited message count', messageCounts[1]); + addLine('Sent message count', this.cachedMessageRecord.length); + + let channel = this.tools.getChannel(mostDeletesChannel.id); + if (channel) addLine('Most deletes', mostDeletesChannel.num + ' ' + this.getLiteralName(channel.guild_id, channel.id)); + if (channel) addLine('Most edits', mostEditsChannel.num + ' ' + this.getLiteralName(channel.guild_id, channel.id)); + + // addLine('Data file size', (this.nodeModules.fs.statSync(this.pluginDir + '/MessageLoggerV2Data.config.json').size / 1024 / 1024).toFixed(2) + 'MB'); + // addLine('Data file size severity', this.slowSaveModeStep == 0 ? 'OK' : this.slowSaveModeStep == 1 ? 'MILD' : this.slowSaveModeStep == 2 ? 'BAD' : 'EXTREME'); + this.createModal({ + confirmText: 'OK', + header: 'Data stats', + size: ZeresPluginLibrary.Modals.ModalSizes.SMALL, + children: [ZeresPluginLibrary.ReactTools.createWrappedElement(elements)], + red: false + }); + } + _findLastIndex(array, predicate) { + let l = array.length; + while (l--) { + if (predicate(array[l], l, array)) + return l; + } + return -1; + } + /* + how it works: + messages, stripped into IDs and times into var IDs: + [1, 2, 3, 4, 5, 6, 7] + ^ ^ + lowestTime highestTime + deletedMessages, stripped into IDs and times into var savedIDs: + sorted by time, newest to oldest + lowest IDX that is higher than lowestTime, unless channelEnd, then it's 0 + highest IDX that is lower than highestTime, unless channelStart, then it's savedIDs.length - 1 + + savedIDs sliced start lowest IDX, end highest IDX + 1 + appended IDs + sorted by time, oldest to newest + iterated, checked if ID is in messages, if not, fetch from this.messageRecord and splice it in at + specified index + */ + reAddDeletedMessages(messages, deletedMessages, channelStart, channelEnd) { + if (!messages.length || !deletedMessages.length) return; + const DISCORD_EPOCH = 14200704e5; + const IDs = []; + const savedIDs = []; + for (let i = 0, len = messages.length; i < len; i++) { + const { id } = messages[i]; + IDs.push({ id: id, time: (id / 4194304) + DISCORD_EPOCH }); + } + for (let i = 0, len = deletedMessages.length; i < len; i++) { + const id = deletedMessages[i]; + const record = this.messageRecord[id]; + if (!record) continue; + if (!record.delete_data) { + /* SOME WIZARD BROKE THE LOGGER LIKE THIS, WTFFFF */ + this.deleteMessageFromRecords(id); + continue; + } + if (record.delete_data.hidden) continue; + savedIDs.push({ id: id, time: (id / 4194304) + DISCORD_EPOCH }); + } + savedIDs.sort((a, b) => a.time - b.time); + if (!savedIDs.length) return; + const { time: lowestTime } = IDs[IDs.length - 1]; + const [{ time: highestTime }] = IDs; + const lowestIDX = channelEnd ? 0 : savedIDs.findIndex(e => e.time > lowestTime); + if (lowestIDX === -1) return; + const highestIDX = channelStart ? savedIDs.length - 1 : this._findLastIndex(savedIDs, e => e.time < highestTime); + if (highestIDX === -1) return; + const reAddIDs = savedIDs.slice(lowestIDX, highestIDX + 1); + reAddIDs.push(...IDs); + reAddIDs.sort((a, b) => b.time - a.time); + for (let i = 0, len = reAddIDs.length; i < len; i++) { + const { id } = reAddIDs[i]; + if (messages.findIndex((e) => e.id === id) !== -1) continue; + const { message } = this.messageRecord[id]; + messages.splice(i, 0, message); + } + } + getLiteralName(guildId, channelId, useTags = false) { + // TODO, custom channel server failure text + const guild = this.tools.getServer(guildId); + const channel = this.tools.getChannel(channelId); // todo + /* if (typeof guildNameBackup !== 'number' && guild && guildNameBackup) */ if (guildId) { + const channelName = (channel ? channel.name : 'unknown-channel'); + const guildName = (guild ? guild.name : 'unknown-server'); + if (useTags && channel) return `${guildName}, <#${channel.id}>`; + return `${guildName}, #${channelName}`; + } else if (channel && channel.name.length) { + return `group ${channel.name}`; + } else if (channel && channel.type == 3) { + let finalGroupName = ''; + for (let i of channel.recipients) { + const user = this.tools.getUser(i); + if (!user) continue; + if (useTags) finalGroupName += ', <@' + user.id + '>'; + else finalGroupName += ',' + user.username; + } + if (!finalGroupName.length) { + return 'unknown group'; + } else { + finalGroupName = finalGroupName.substr(1); + if (useTags) return `group ${finalGroupName}`; + finalGroupName = finalGroupName.length > 10 ? finalGroupName.substr(0, 10 - 1) + '...' : finalGroupName; + return `group ${finalGroupName}`; + } + } else if (channel && channel.recipients) { + const user = this.tools.getUser(channel.recipients[0]); + if (!user) return 'DMs'; + if (useTags) return `<@${user.id}> DMs`; + return `${user.username} DMs`; + } else { + return 'DMs'; + } + } + saveDeletedMessage(message, targetMessageRecord) { + let result = this.createMiniFormattedData(message); + result.delete_data = {}; + const id = message.id; + const channelId = message.channel_id; + result.delete_data.time = new Date().getTime(); + result.ghost_pinged = result.local_mentioned; // it's simple bruh + if (!Array.isArray(targetMessageRecord[channelId])) targetMessageRecord[channelId] = []; + if (this.messageRecord[id]) { + const record = this.messageRecord[id]; + record.delete_data = result.delete_data; + record.ghost_pinged = result.ghost_pinged; + } else { + this.messageRecord[id] = result; + } + if (this.messageRecord[id].message.attachments) { + const attachments = this.messageRecord[id].message.attachments; + for (let i = 0; i < attachments.length; i++) { + attachments[i].url = attachments[i].proxy_url; // proxy url lasts longer + } + } + if (this.settings.cacheAllImages) this.cacheMessageImages(this.messageRecord[id].message); + targetMessageRecord[channelId].push(id); + } + createButton(label, callback) { + const classes = this.createButton.classes; + const ret = this.parseHTML(``); + if (callback) ret.addEventListener('click', callback); + return ret; + } + createModal(options, image, name) { + const modal = image ? this.createModal.imageModal : this.createModal.confirmationModal; + this.ModalStack.openModal(props => ZeresPluginLibrary.DiscordModules.React.createElement(modal, Object.assign({}, options, props, options.onClose ? { onClose: options.onClose } : {})), { modalKey: name }); + } + getMessageAny(id) { + const record = this.messageRecord[id]; + if (!record) return this.cachedMessageRecord.find(m => m.id == id); + return record.message; + } + cacheImage(url, attachmentIdx, attachmentId, messageId, channelId, attempts = 0) { + this.nodeModules.request({ url: url, encoding: null }, (err, res, buffer) => { + if (err || res.statusCode != 200) { + if (res.statusCode == 404 || res.statusCode == 403) return; + attempts++; + if (attempts > 3) return ZeresPluginLibrary.Logger.warn(this.getName(), `Failed to get image ${attachmentId} for caching, error code ${res.statusCode}`); + return setTimeout(() => this.cacheImage(url, attachmentIdx, attachmentId, messageId, channelId, attempts), 1000); + } + const fileExtension = url.match(/\.[0-9a-z]+$/i)[0]; + this.nodeModules.fs.writeFileSync(this.settings.imageCacheDir + `/${attachmentId}${fileExtension}`, buffer, { encoding: null }); + }); + } + cacheMessageImages(message) { + // don't block it, ugly but works, might rework later + setTimeout(() => { + for (let i = 0; i < message.attachments.length; i++) { + const attachment = message.attachments[i]; + if (!this.isImage(attachment.url)) continue; + this.cacheImage(attachment.url, i, attachment.id, message.id, message.channel_id); + } + }, 0); + } + /* ==================================================-|| END MISC ||-================================================== */ + /* ==================================================-|| START MESSAGE MANAGMENT ||-================================================== */ + deleteMessageFromRecords(id) { + const record = this.messageRecord[id]; + if (!record) { + for (let map of [this.deletedMessageRecord, this.editedMessageRecord, this.purgedMessageRecord]) { + for (let channelId in map) { + const index = map[channelId].findIndex(m => m === id); + if (index == -1) continue; + map[channelId].splice(index, 1); + if (!map[channelId].length) delete map[channelId]; + } + } + return; + } + // console.log('Deleting', record); + const channelId = record.message.channel_id; + for (let map of [this.deletedMessageRecord, this.editedMessageRecord, this.purgedMessageRecord]) { + if (!map[channelId]) continue; + const index = map[channelId].findIndex(m => m === id); + if (index == -1) continue; + map[channelId].splice(index, 1); + if (!map[channelId].length) delete map[channelId]; + } + delete this.messageRecord[id]; + } + handleMessagesCap() { + try { + // TODO: add empty record and infinite loop checking for speed improvements + const extractAllMessageIds = map => { + let ret = []; + for (let channelId in map) { + for (let messageId of map[channelId]) { + ret.push(messageId); + } + } + return ret; + }; + if (this.cachedMessageRecord.length > this.settings.messageCacheCap) this.cachedMessageRecord.splice(0, this.cachedMessageRecord.length - this.settings.messageCacheCap); + let changed = false; + const deleteMessages = map => { + this.sortMessagesByAge(map); + const toDelete = map.length - this.settings.savedMessagesCap; + for (let i = map.length - 1, deleted = 0; i >= 0 && deleted != toDelete; i--, deleted++) { + this.deleteMessageFromRecords(map[i]); + } + changed = true; + }; + const handleInvalidEntries = map => { + for (let channelId in map) { + for (let messageIdIdx = map[channelId].length - 1; messageIdIdx >= 0; messageIdIdx--) { + if (!Array.isArray(map[channelId])) { + delete map[channelId]; + changed = true; + continue; + } + if (!this.messageRecord[map[channelId][messageIdIdx]]) { + map[channelId].splice(messageIdIdx, 1); + changed = true; + } + } + if (!map[channelId].length) { + delete map[channelId]; + changed = true; + } + } + }; + for (let map of [this.deletedMessageRecord, this.editedMessageRecord, this.purgedMessageRecord]) handleInvalidEntries(map); + // I have no idea how to optimize this, HELP! + //const checkIsInRecords = (channelId, messageId) => { + // // for (let map of [this.deletedMessageRecord, this.editedMessageRecord, this.purgedMessageRecord]) if (map[channelId] && map[channelId].indexOf(messageId) !== -1) return true; + // let map = this.deletedMessageRecord[channelId]; + // if (map && map.indexOf(messageId) !== -1) return true; + // map = this.editedMessageRecord[channelId]; + // if (map && map.indexOf(messageId) !== -1) return true; + // map = this.purgedMessageRecord[channelId]; + // if (map && map.indexOf(messageId) !== -1) return true; + // return false; + //}; + + //for (const messageId in this.messageRecord) { + // if (!checkIsInRecords(this.messageRecord[messageId].message.channel_id, messageId)) {/* delete this.messageRecord[messageId]; */ } + //} + let deletedMessages = extractAllMessageIds(this.deletedMessageRecord); + let editedMessages = extractAllMessageIds(this.editedMessageRecord); + let purgedMessages = extractAllMessageIds(this.purgedMessageRecord); + for (let map of [deletedMessages, editedMessages, purgedMessages]) if (map.length > this.settings.savedMessagesCap) deleteMessages(map); + if (changed) this.saveData(); + if (!this.settings.cacheAllImages) return; + if (!this.settings.dontDeleteCachedImages) { + const savedImages = this.nodeModules.fs.readdirSync(this.settings.imageCacheDir); + const msgs = Object.values(this.messageRecord) + .filter(e => e.delete_data) + .map(({ message: { attachments } }) => attachments) + .filter(e => e.length); + for (let img of savedImages) { + const [attId] = img.split('.'); + if (isNaN(attId)) continue; + let found = false; + for (let i = 0, len = msgs.length; i < len; i++) { + if (msgs[i].findIndex(({ id }) => id === attId) !== -1) { + found = true; + break; + } + } + if (found) continue; + this.nodeModules.fs.unlink(`${this.settings.imageCacheDir}/${img}`, e => e && ZeresPluginLibrary.Logger.err(this.getName(), 'Error deleting unreferenced image, what the shit', e)); + } + } + // 10 minutes + for (let id in this.editHistoryAntiSpam) if (new Date().getTime() - this.editHistoryAntiSpam[id].times[0] < 10 * 60 * 1000) delete this.editHistoryAntiSpam[id]; + } catch (e) { + ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Error clearing out data', e); + } + } + /* ==================================================-|| END MESSAGE MANAGMENT ||-================================================== */ + onDispatchEvent(args, callDefault) { + const dispatch = args[0]; + + if (!dispatch) return callDefault(...args); + + try { + if (dispatch.type === 'MESSAGE_LOGGER_V2_SELF_TEST') { + clearTimeout(this.selfTestTimeout); + //console.log('Self test OK'); + this.selfTestFailures = 0; + return; + } + // if (dispatch.type == 'EXPERIMENT_TRIGGER') return callDefault(...args); + // console.log('INFO: onDispatchEvent -> dispatch', dispatch); + if (dispatch.type === 'CHANNEL_SELECT') { + callDefault(...args); + this.selectedChannel = this.getSelectedTextChannel(); + return; + } + + if (dispatch.ML2 && dispatch.type === 'MESSAGE_DELETE') return callDefault(...args); + + if (dispatch.type !== 'MESSAGE_CREATE' && dispatch.type !== 'MESSAGE_DELETE' && dispatch.type !== 'MESSAGE_DELETE_BULK' && dispatch.type !== 'MESSAGE_UPDATE' && dispatch.type !== 'LOAD_MESSAGES_SUCCESS') return callDefault(...args); + + // console.log('INFO: onDispatchEvent -> dispatch', dispatch); + + if (dispatch.message && (dispatch.message.type !== 0 && dispatch.message.type !== 19)) return callDefault(...args); // anti other shit 1 + + const channel = this.tools.getChannel(dispatch.message ? dispatch.message.channel_id : dispatch.channelId); + if (!channel) return callDefault(...args); + const guild = channel.guild_id ? this.tools.getServer(channel.guild_id) : false; + + let author = dispatch.message && dispatch.message.author ? this.tools.getUser(dispatch.message.author.id) : false; + if (!author) author = ((this.channelMessages[channel.id] || { _map: {} })._map[dispatch.message ? dispatch.message.id : dispatch.id] || {}).author; + if (!author) { + // last ditch attempt + let message = this.getCachedMessage(dispatch.id); + if (message) author = this.tools.getUser(message.author.id); + } + + if (!author && !(dispatch.type == 'LOAD_MESSAGES_SUCCESS' || dispatch.type == 'MESSAGE_DELETE_BULK')) return callDefault(...args); + + const isLocalUser = author && author.id === this.localUser.id; + + if (author && author.bot && this.settings.ignoreBots) return callDefault(...args); + if (author && isLocalUser && this.settings.ignoreSelf) return callDefault(...args); + if (author && this.settings.ignoreBlockedUsers && this.tools.isBlocked(author.id) && !isLocalUser) return callDefault(...args); + if (author && author.avatar === 'clyde') return callDefault(...args); + + if (this.settings.ignoreLocalEdits && dispatch.type === 'MESSAGE_UPDATE' && isLocalUser) return callDefault(...args); + if (this.settings.ignoreLocalDeletes && dispatch.type === 'MESSAGE_DELETE' && isLocalUser && this.localDeletes.findIndex(m => m === dispatch.id) !== -1) return callDefault(...args); + + let guildIsMutedReturn = false; + let channelIgnoreReturn = false; + + const isInWhitelist = id => this.settings.whitelist.findIndex(m => m === id) != -1; + const isInBlacklist = id => this.settings.blacklist.findIndex(m => m === id) != -1; + const guildWhitelisted = guild && isInWhitelist(guild.id); + const channelWhitelisted = isInWhitelist(channel.id); + + const guildBlacklisted = guild && isInBlacklist(guild.id); + const channelBlacklisted = isInBlacklist(channel.id); + + let doReturn = false; + + if (guild) { + guildIsMutedReturn = this.settings.ignoreMutedGuilds && this.muteModule.isMuted(guild.id); + channelIgnoreReturn = (this.settings.ignoreNSFW && channel.nsfw && !channelWhitelisted) || (this.settings.ignoreMutedChannels && (this.muteModule.isChannelMuted(guild.id, channel.id) || (channel.parent_id && this.muteModule.isChannelMuted(channel.parent_id)))); + } + + if (!((this.settings.alwaysLogSelected && this.selectedChannel && this.selectedChannel.id == channel.id) || (this.settings.alwaysLogDM && !guild))) { + if (guildBlacklisted) { + if (!channelWhitelisted) doReturn = true; // not whitelisted + } else if (guildWhitelisted) { + if (channelBlacklisted) doReturn = true; // channel blacklisted + if (channelIgnoreReturn && !channelWhitelisted) doReturn = true; + } else { + if (this.settings.onlyLogWhitelist) { + if (!channelWhitelisted) doReturn = true; // guild not in either list, channel not whitelisted + } else { + if (channelBlacklisted) doReturn = true; // channel blacklisted + if (channelIgnoreReturn || guildIsMutedReturn) { + if (!channelWhitelisted) doReturn = true; + } + } + } + } + + if (doReturn && this.settings.alwaysLogGhostPings) { + if (dispatch.type === 'MESSAGE_DELETE') { + const deleted = (this.tempEditedMessageRecord[dispatch.id] && this.tempEditedMessageRecord[dispatch.id].message) || this.getCachedMessage(dispatch.id, dispatch.channelId); + if (!deleted || (deleted.type !== 0 && deleted.type !== 19)) return callDefault(...args); // nothing we can do past this point.. + if (!this.tools.isMentioned(deleted, this.localUser.id)) return callDefault(...args); + const record = this.messageRecord[dispatch.id]; + if ((!this.selectedChannel || this.selectedChannel.id != channel.id) && (guild ? this.settings.toastToggles.ghostPings : this.settings.toastTogglesDMs.ghostPings) && (!record || !record.ghost_pinged)) { + XenoLib.Notifications.warning(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id, true)}`, { timeout: 0, onClick: () => this.openWindow('ghostpings'), onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id), channelId: channel.id }); + if (!this.settings.useNotificationsInstead) { + this.showToast(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id)}`, { + type: 'warning', + onClick: () => this.openWindow('ghostpings'), + onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id), + timeout: 4500 + }); + } + } + this.saveDeletedMessage(deleted, this.deletedMessageRecord); + this.saveData(); + if (this.currentChannel() && this.currentChannel().id === dispatch.channelId) ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE', id: dispatch.id }); + } else if (dispatch.type === 'MESSAGE_UPDATE') { + if (!dispatch.message.edited_timestamp) { + if (dispatch.message.embeds) { + let last = this.getCachedMessage(dispatch.message.id); + if (last) last.embeds = dispatch.message.embeds.map(this.cleanupEmbed); + } + return callDefault(...args); + } + let isSaved = this.getEditedMessage(dispatch.message.id, channel.id); + const last = this.getCachedMessage(dispatch.message.id, channel.id); + const lastEditedSaved = isSaved || this.tempEditedMessageRecord[dispatch.message.id]; + // if we have lastEdited then we can still continue as we have all the data we need to process it. + if (!last && !lastEditedSaved) return callDefault(...args); // nothing we can do past this point.. + + if (isSaved && !lastEditedSaved.local_mentioned) { + lastEditedSaved.message.content = dispatch.message.content; // don't save history, just the value so we don't confuse the user + return callDefault(...args); + } + + let ghostPinged = false; + if (lastEditedSaved) { + // last is not needed, we have all the data already saved + if (lastEditedSaved.message.content === dispatch.message.content) return callDefault(...args); // we don't care about that + lastEditedSaved.edit_history.push({ + content: lastEditedSaved.message.content, + time: new Date().getTime() + }); + lastEditedSaved.message.content = dispatch.message.content; + ghostPinged = !lastEditedSaved.ghost_pinged && lastEditedSaved.local_mentioned && !this.tools.isMentioned(dispatch.message, this.localUser.id); + } else { + if (last.content === dispatch.message.content) return callDefault(...args); // we don't care about that + let data = this.createMiniFormattedData(last); + data.edit_history = [ + { + content: last.content, + time: new Date().getTime() + } + ]; + data.message.content = dispatch.message.content; + this.tempEditedMessageRecord[data.message.id] = data; + ghostPinged = this.tools.isMentioned(last, this.localUser.id) && !this.tools.isMentioned(dispatch.message, this.localUser.id); + } + + if (isSaved) this.saveData(); + + if (!ghostPinged) return callDefault(...args); + + if (!isSaved) { + const data = this.tempEditedMessageRecord[dispatch.message.id]; + data.ghost_pinged = true; + this.messageRecord[dispatch.message.id] = data; + if (!this.editedMessageRecord[channel.id]) this.editedMessageRecord[channel.id] = []; + this.editedMessageRecord[channel.id].push(dispatch.message.id); + this.saveData(); + } else { + const lastEdited = this.getEditedMessage(dispatch.message.id, channel.id); + if (!lastEdited) return callDefault(...args); + lastEdited.ghost_pinged = true; + this.saveData(); + } + + if ((!this.selectedChannel || this.selectedChannel.id != channel.id) && (guild ? this.settings.toastToggles.ghostPings : this.settings.toastTogglesDMs.ghostPings)) { + XenoLib.Notifications.warning(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id, true)}`, { timeout: 0, onClick: () => this.openWindow('ghostpings'), onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id), channelId: channel.id }); + if (!this.settings.useNotificationsInstead) { + this.showToast(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id)}`, { + type: 'warning', + onClick: () => this.openWindow('ghostpings'), + onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id), + timeout: 4500 + }); + } + } + } else if (dispatch.type == 'MESSAGE_CREATE' && dispatch.message && (dispatch.message.content.length || (dispatch.attachments && dispatch.attachments.length) || (dispatch.embeds && dispatch.embeds.length)) && dispatch.message.state != 'SENDING' && !dispatch.optimistic && (dispatch.message.type === 0 || dispatch.message.type === 19) && this.tools.isMentioned(dispatch.message, this.localUser.id)) { + if (this.cachedMessageRecord.findIndex(m => m.id === dispatch.message.id) != -1) return callDefault(...args); + this.cachedMessageRecord.push(dispatch.message); + } + } + if (doReturn) return callDefault(...args); + + if (dispatch.type == 'LOAD_MESSAGES_SUCCESS') { + if (!this.settings.restoreDeletedMessages) return callDefault(...args); + if (dispatch.jump && dispatch.jump.ML2) delete dispatch.jump; + const deletedMessages = this.deletedMessageRecord[channel.id]; + const purgedMessages = this.purgedMessageRecord[channel.id]; + try { + const recordIDs = [...(deletedMessages || []), ...(purgedMessages || [])]; + const fetchUser = id => this.tools.getUser(id) || dispatch.messages.find(e => e.author.id === id) + for (let i = 0, len = recordIDs.length; i < len; i++) { + const id = recordIDs[i]; + if (!this.messageRecord[id]) continue; + const { message } = this.messageRecord[id]; + for (let j = 0, len2 = message.mentions.length; j < len2; j++) { + const user = message.mentions[j]; + const cachedUser = fetchUser(user.id || user); + if (cachedUser) message.mentions[j] = this.cleanupUserObject(cachedUser); + } + const author = fetchUser(message.author.id); + if (!author) continue; + message.author = this.cleanupUserObject(author); + } + } catch { } + if ((!deletedMessages && !purgedMessages) || (!this.settings.showPurgedMessages && !this.settings.showDeletedMessages)) return callDefault(...args); + if (this.settings.showDeletedMessages && deletedMessages) this.reAddDeletedMessages(dispatch.messages, deletedMessages, !dispatch.hasMoreAfter && !dispatch.isBefore, !dispatch.hasMoreBefore && !dispatch.isAfter); + if (this.settings.showPurgedMessages && purgedMessages) this.reAddDeletedMessages(dispatch.messages, purgedMessages, !dispatch.hasMoreAfter && !dispatch.isBefore, !dispatch.hasMoreBefore && !dispatch.isAfter); + return callDefault(...args); + } + + const notificationsBlacklisted = this.settings.notificationBlacklist.indexOf(channel.id) !== -1 || (guild && this.settings.notificationBlacklist.indexOf(guild.id) !== -1); + + if (dispatch.type == 'MESSAGE_DELETE') { + const deleted = this.getCachedMessage(dispatch.id, dispatch.channelId); + + if (this.settings.aggresiveMessageCaching) { + const channelMessages = this.channelMessages[channel.id]; + if (!channelMessages || !channelMessages.ready) this.cacheChannelMessages(channel.id); + } + + if (!deleted) return callDefault(...args); // nothing we can do past this point.. + + if (this.deletedMessageRecord[channel.id] && this.deletedMessageRecord[channel.id].findIndex(m => m === deleted.id) != -1) { + if (!this.settings.showDeletedMessages) callDefault(...args); + return; + } + + if (deleted.type !== 0 && deleted.type !== 19) return callDefault(...args); + + if (this.settings.showDeletedCount) { + if (!this.deletedChatMessagesCount[channel.id]) this.deletedChatMessagesCount[channel.id] = 0; + if (!this.selectedChannel || this.selectedChannel.id != channel.id) this.deletedChatMessagesCount[channel.id]++; + } + if (!notificationsBlacklisted) { + if (guild ? this.settings.toastToggles.deleted && ((isLocalUser && !this.settings.toastToggles.disableToastsForLocal) || !isLocalUser) : this.settings.toastTogglesDMs.deleted && !isLocalUser) { + if (this.settings.useNotificationsInstead) { + XenoLib.Notifications.danger(`Message deleted from ${this.getLiteralName(channel.guild_id, channel.id, true)}`, { + onClick: () => this.openWindow('deleted'), + onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id), + timeout: 4500 + }); + } else { + this.showToast(`Message deleted from ${this.getLiteralName(channel.guild_id, channel.id)}`, { + type: 'error', + onClick: () => this.openWindow('deleted'), + onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id), + timeout: 4500 + }); + } + } + } + + const record = this.messageRecord[dispatch.id]; + + if ((!this.selectedChannel || this.selectedChannel.id != channel.id) && (guild ? this.settings.toastToggles.ghostPings : this.settings.toastTogglesDMs.ghostPings) && (!record || !record.ghost_pinged) && this.tools.isMentioned(deleted, this.localUser.id)) { + XenoLib.Notifications.warning(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id, true)}`, { timeout: 0, onClick: () => this.openWindow('ghostpings'), onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id), channelId: dispatch.channelId }); + if (!this.settings.useNotificationsInstead) { + this.showToast(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id)}`, { + type: 'warning', + onClick: () => this.openWindow('ghostpings'), + onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id), + timeout: 4500 + }); + } + } + + this.saveDeletedMessage(deleted, this.deletedMessageRecord); + // if (this.settings.cacheAllImages) this.cacheImages(deleted); + if (!this.settings.showDeletedMessages) callDefault(...args); + else if (this.currentChannel() && this.currentChannel().id === dispatch.channelId) ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE', id: dispatch.id }); + this.saveData(); + } else if (dispatch.type == 'MESSAGE_DELETE_BULK') { + if (this.settings.showDeletedCount) { + if (!this.deletedChatMessagesCount[channel.id]) this.deletedChatMessagesCount[channel.id] = 0; + if (!this.selectedChannel || this.selectedChannel.id != channel.id) this.deletedChatMessagesCount[channel.id] += dispatch.ids.length; + } + + let failedMessage = false; + + for (let i = 0; i < dispatch.ids.length; i++) { + const purged = this.getCachedMessage(dispatch.ids[i], channel.id); + if (!purged) { + failedMessage = true; + continue; + } + this.saveDeletedMessage(purged, this.purgedMessageRecord); + if (this.currentChannel() && this.currentChannel().id === dispatch.channelId) ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE', id: purged.id }); + } + + if (failedMessage && this.aggresiveMessageCaching) + // forcefully cache the channel in case there are active convos there + this.cacheChannelMessages(channel.id); + else if (this.settings.aggresiveMessageCaching) { + const channelMessages = this.channelMessages[channel.id]; + if (!channelMessages || !channelMessages.ready) this.cacheChannelMessages(channel.id); + } + if (!notificationsBlacklisted) { + if (guild ? this.settings.toastToggles.deleted : this.settings.toastTogglesDMs.deleted) { + if (this.settings.useNotificationsInstead) { + XenoLib.Notifications.danger(`${dispatch.ids.length} messages bulk deleted from ${this.getLiteralName(channel.guild_id, channel.id, true)}`, { + onClick: () => this.openWindow('purged'), + onContext: () => this.jumpToMessage(channel.id, undefined, guild && guild.id), + timeout: 4500 + }); + } else { + this.showToast(`${dispatch.ids.length} messages bulk deleted from ${this.getLiteralName(channel.guild_id, channel.id)}`, { + type: 'error', + onClick: () => this.openWindow('purged'), + onContext: () => this.jumpToMessage(channel.id, undefined, guild && guild.id), + timeout: 4500 + }); + } + } + } + if (!this.settings.showPurgedMessages) callDefault(...args); + this.saveData(); + } else if (dispatch.type == 'MESSAGE_UPDATE') { + if (!dispatch.message.edited_timestamp) { + if (dispatch.message.embeds) { + let last = this.getCachedMessage(dispatch.message.id); + if (last) last.embeds = dispatch.message.embeds.map(this.cleanupEmbed); + } + return callDefault(...args); + } + + if (this.settings.showEditedCount) { + if (!this.editedChatMessagesCount[channel.id]) this.editedChatMessagesCount[channel.id] = 0; + if (!this.selectedChannel || this.selectedChannel.id != channel.id) this.editedChatMessagesCount[channel.id]++; + } + + if (this.settings.aggresiveMessageCaching) { + const channelMessages = this.channelMessages[channel.id]; + if (!channelMessages || !channelMessages.ready) this.cacheChannelMessages(channel.id); + } + + const last = this.getCachedMessage(dispatch.message.id, channel.id); + const lastEditedSaved = this.getEditedMessage(dispatch.message.id, channel.id); + + // if we have lastEdited then we can still continue as we have all the data we need to process it. + if (!last && !lastEditedSaved) return callDefault(...args); // nothing we can do past this point.. + let ghostPinged = false; + if (lastEditedSaved) { + // last is not needed, we have all the data already saved + // console.log(lastEditedSaved.message); + // console.log(dispatch.message); + if (lastEditedSaved.message.content === dispatch.message.content) { + return callDefault(...args); // we don't care about that + } + lastEditedSaved.edit_history.push({ + content: lastEditedSaved.message.content, + time: new Date().getTime() + }); + lastEditedSaved.message.content = dispatch.message.content; + ghostPinged = !lastEditedSaved.ghost_pinged && lastEditedSaved.local_mentioned && !this.tools.isMentioned(dispatch.message, this.localUser.id); + if (ghostPinged) lastEditedSaved.ghost_pinged = true; + } else { + if (last.content === dispatch.message.content) { + return callDefault(...args); // we don't care about that + } + let data = this.createMiniFormattedData(last); + data.edit_history = [ + { + content: last.content, + time: new Date().getTime() + } + ]; + ghostPinged = this.tools.isMentioned(last, this.localUser.id) && !this.tools.isMentioned(dispatch.message, this.localUser.id); + data.message.content = dispatch.message.content; + if (ghostPinged) data.ghost_pinged = true; + this.messageRecord[data.message.id] = data; + if (!this.editedMessageRecord[channel.id]) this.editedMessageRecord[channel.id] = []; + this.editedMessageRecord[channel.id].push(data.message.id); + } + if (!notificationsBlacklisted) { + if (guild ? this.settings.toastToggles.edited && ((isLocalUser && !this.settings.toastToggles.disableToastsForLocal) || !isLocalUser) : this.settings.toastTogglesDMs.edited && !isLocalUser) { + if (!this.settings.blockSpamEdit) { + if (!this.editHistoryAntiSpam[author.id]) { + this.editHistoryAntiSpam[author.id] = { + blocked: false, + times: [new Date().getTime()] + }; + } else { + this.editHistoryAntiSpam[author.id].times.push(new Date().getTime()); + } + if (this.editHistoryAntiSpam[author.id].times.length > 10) this.editHistoryAntiSpam[author.id].times.shift(); + if (this.editHistoryAntiSpam[author.id].times.length === 10 && new Date().getTime() - this.editHistoryAntiSpam[author.id].times[0] < 60 * 1000) { + if (!this.editHistoryAntiSpam[author.id].blocked) { + if (this.settings.useNotificationsInstead) { + XenoLib.Notifications.warning(`Edit notifications from <@${author.id}> have been temporarily blocked for 1 minute.`, { + timeout: 7500, + channelId: channel.id + }); + } else { + this.showToast(`Edit notifications from ${author.username} have been temporarily blocked for 1 minute.`, { + type: 'warning', + timeout: 7500 + }); + } + this.editHistoryAntiSpam[author.id].blocked = true; + } + } else if (this.editHistoryAntiSpam[author.id].blocked) { + this.editHistoryAntiSpam[author.id].blocked = false; + this.editHistoryAntiSpam[author.id].times = []; + } + } + if (this.settings.blockSpamEdit || !this.editHistoryAntiSpam[author.id].blocked) { + if (this.settings.useNotificationsInstead) { + XenoLib.Notifications.info(`Message edited in ${this.getLiteralName(channel.guild_id, channel.id, true)}`, { + onClick: () => this.openWindow('edited'), + onContext: () => this.jumpToMessage(channel.id, dispatch.message.id, guild && guild.id), + timeout: 4500 + }); + } else { + this.showToast(`Message edited in ${this.getLiteralName(channel.guild_id, channel.id)}`, { + type: 'info', + onClick: () => this.openWindow('edited'), + onContext: () => this.jumpToMessage(channel.id, dispatch.message.id, guild && guild.id), + timeout: 4500 + }); + } + } + } + } + if ((!this.selectedChannel || this.selectedChannel.id != channel.id) && (guild ? this.settings.toastToggles.ghostPings : this.settings.toastTogglesDMs.ghostPings) && ghostPinged) { + XenoLib.Notifications.warning(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id, true)}`, { timeout: 0, onClick: () => this.openWindow('ghostpings'), onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id), channelId: dispatch.channelId }); + if (!this.settings.useNotificationsInstead) { + this.showToast(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id)}`, { + type: 'warning', + onClick: () => this.openWindow('ghostpings'), + onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id), + timeout: 4500 + }); + } + } + this.saveData(); + return callDefault(...args); + } else if (dispatch.type == 'MESSAGE_CREATE' && dispatch.message && (dispatch.message.content.length || (dispatch.attachments && dispatch.attachments.length) || (dispatch.embeds && dispatch.embeds.length)) && !global.ohgodohfuck && dispatch.message.state != 'SENDING' && !dispatch.optimistic && (dispatch.message.type === 0 || dispatch.message.type === 19)) { + if (this.cachedMessageRecord.findIndex(m => m.id === dispatch.message.id) != -1) return callDefault(...args); + this.cachedMessageRecord.push(dispatch.message); + + /* if (this.menu.open && this.menu.selectedTab == 'sent') this.refilterMessages(); */ + + if (this.settings.aggresiveMessageCaching) { + const channelMessages = this.channelMessages[channel.id]; + if (!channelMessages || !channelMessages.ready) this.cacheChannelMessages(channel.id); + } + if (!notificationsBlacklisted) { + if ((guild ? this.settings.toastToggles.sent : this.settings.toastTogglesDMs.sent) && (!this.selectedChannel || this.selectedChannel.id != channel.id)) { + if (this.settings.useNotificationsInstead) { + XenoLib.Notifications.info(`Message sent in ${this.getLiteralName(channel.guild_id, channel.id, true)}`, { onClick: () => this.openWindow('sent'), onContext: () => this.jumpToMessage(channel.id, dispatch.message.id, guild && guild.id), timeout: 4500 }); + } else { + this.showToast(`Message sent in ${this.getLiteralName(channel.guild_id, channel.id)}`, { type: 'info', onClick: () => this.openWindow('sent'), onContext: () => this.jumpToMessage(channel.id, dispatch.message.id, guild && guild.id), timeout: 4500 }); + } + } + } + callDefault(...args); + } else callDefault(...args); + } catch (err) { + ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Error in onDispatchEvent', err); + } + } + /* ==================================================-|| START MENU ||-================================================== */ + processUserRequestQueue() { + return; + if (!this.processUserRequestQueue.queueIntervalTime) this.processUserRequestQueue.queueIntervalTime = 500; + if (this.menu.queueInterval) return; + const messageDataManager = () => { + if (!this.menu.userRequestQueue.length) { + clearInterval(this.menu.queueInterval); + this.menu.queueInterval = 0; + return; + } + const data = this.menu.userRequestQueue.shift(); + this.tools + .getUserAsync(data.id) + .then(res => { + for (let ss of data.success) ss(res); + }) + .catch(reason => { + if (reason.status == 429 && typeof reason.body.retry_after === 'number') { + clearInterval(this.menu.queueInterval); + this.menu.queueInterval = 0; + this.processUserRequestQueue.queueIntervalTime += 50; + setTimeout(messageDataManager, reason.body.retry_after); + ZeresPluginLibrary.Logger.warn(this.getName(), 'Rate limited, retrying in', reason.body.retry_after, 'ms'); + this.menu.userRequestQueue.push(data); + return; + } + ZeresPluginLibrary.Logger.warn(this.getName(), `Failed to get info for ${data.username}, reason:`, reason); + for (let ff of data.fail) ff(); + }); + }; + this.menu.queueInterval = setInterval(messageDataManager, this.processUserRequestQueue.queueIntervalTime); + } + patchMessages() { + const Tooltip = ZeresPluginLibrary.WebpackModules.getByDisplayName('Tooltip'); + const TimeUtils = ZeresPluginLibrary.WebpackModules.getByProps('dateFormat'); + /* suck it you retarded asshole devilfuck */ + const SuffixEdited = ZeresPluginLibrary.DiscordModules.React.memo(e => ZeresPluginLibrary.DiscordModules.React.createElement(Tooltip, { text: e.timestamp ? TimeUtils.dateFormat(e.timestamp, 'LLLL') : null }, tt => ZeresPluginLibrary.DiscordModules.React.createElement('time', Object.assign({ dateTime: e.timestamp.toISOString(), className: this.multiClasses.edited, role: 'note' }, tt), `(${ZeresPluginLibrary.DiscordModules.LocaleManager.Messages.MESSAGE_EDITED})`))); + SuffixEdited.displayName = 'SuffixEdited'; + const parseContent = ZeresPluginLibrary.WebpackModules.getByProps('renderMessageMarkupToAST').default; + const MessageContent = ZeresPluginLibrary.WebpackModules.find(m => m.type && m.type.displayName === 'MessageContent' || m.__powercordOriginal_type && m.__powercordOriginal_type.displayName === 'MessageContent'); + const MemoMessage = ZeresPluginLibrary.WebpackModules.find(m => m.type && m.type.toString().indexOf('useContextMenuMessage') !== -1 || m.__powercordOriginal_type && m.__powercordOriginal_type.toString().indexOf('useContextMenuMessage') !== -1); + if (!MessageContent || !MemoMessage) return XenoLib.Notifications.error('Failed to patch message components, edit history and deleted tint will not show!', { timeout: 0 }); + this.unpatches.push( + ZeresPluginLibrary.Patcher.after(this.getName(), MessageContent, 'type', (_, [props], ret) => { + const forceUpdate = ZeresPluginLibrary.DiscordModules.React.useState()[1]; + ZeresPluginLibrary.DiscordModules.React.useEffect( + function () { + function callback(e) { + if (!e || !e.id || e.id === props.message.id) { + forceUpdate({}); + } + } + ZeresPluginLibrary.DiscordModules.Dispatcher.subscribe('MLV2_FORCE_UPDATE_MESSAGE_CONTENT', callback); + return function () { + ZeresPluginLibrary.DiscordModules.Dispatcher.unsubscribe('MLV2_FORCE_UPDATE_MESSAGE_CONTENT', callback); + }; + }, + [props.message.id, forceUpdate] + ); + if (!this.settings.showEditedMessages) return; + if (!this.editedMessageRecord[props.message.channel_id] || this.editedMessageRecord[props.message.channel_id].indexOf(props.message.id) === -1) return; + const record = this.messageRecord[props.message.id]; + if (!record || record.edits_hidden || !Array.isArray(ret.props.children)) return; + const createEditedMessage = (edit, editNum, isSingular, noSuffix) => + ZeresPluginLibrary.DiscordModules.React.createElement( + XenoLib.ReactComponents.ErrorBoundary, + { label: 'Edit history' }, + ZeresPluginLibrary.DiscordModules.React.createElement( + Tooltip, + { + text: !!record.delete_data ? null : 'Edited: ' + this.createTimeStamp(edit.time), + position: 'left', + hideOnClick: true + }, + _ => + ZeresPluginLibrary.DiscordModules.React.createElement( + 'div', + { + ..._, + className: XenoLib.joinClassNames({ [this.style.editedCompact]: props.compact && !isSingular, [this.style.edited]: !isSingular }), + editNum + }, + parseContent({ channel_id: props.message.channel_id, mentionChannels: props.message.mentionChannels, content: edit.content, embeds: [] }).content, + noSuffix + ? null + : ZeresPluginLibrary.DiscordModules.React.createElement(SuffixEdited, { + timestamp: this.tools.createMomentObject(edit.time) + }) + ) + ) + ); + ret.props.className = XenoLib.joinClassNames(ret.props.className, this.style.edited); + const modifier = this.editModifiers[props.message.id]; + if (modifier) { + ret.props.children = [createEditedMessage(record.edit_history[modifier.editNum], modifier.editNum, true, modifier.noSuffix)]; + return; + } + const oContent = Array.isArray(ret.props.children[0]) ? ret.props.children[0] : ret.props.children[1]; + const edits = []; + let i = 0; + let max = record.edit_history.length; + if (this.settings.maxShownEdits) { + if (record.edit_history.length > this.settings.maxShownEdits) { + if (this.settings.hideNewerEditsFirst) { + max = this.settings.maxShownEdits; + } else { + i = record.edit_history.length - this.settings.maxShownEdits; + } + } + } + for (; i < max; i++) { + const edit = record.edit_history[i]; + if (!edit) continue; + let editNum = i; + edits.push(createEditedMessage(edit, editNum)); + } + ret.props.children = [edits, oContent]; + }) + ); + this.unpatches.push( + ZeresPluginLibrary.Patcher.after(this.getName(), MemoMessage, 'type', (_, [props], ret) => { + const forceUpdate = ZeresPluginLibrary.DiscordModules.React.useState()[1]; + ZeresPluginLibrary.DiscordModules.React.useEffect( + function () { + function callback(e) { + if (!e || !e.id || e.id === props.message.id) forceUpdate({}); + } + ZeresPluginLibrary.DiscordModules.Dispatcher.subscribe('MLV2_FORCE_UPDATE_MESSAGE', callback); + return function () { + ZeresPluginLibrary.DiscordModules.Dispatcher.unsubscribe('MLV2_FORCE_UPDATE_MESSAGE', callback); + }; + }, + [props.message.id, forceUpdate] + ); + const record = this.messageRecord[props.message.id]; + if (!record || !record.delete_data) return; + if (this.noTintIds.indexOf(props.message.id) !== -1) return; + ret.props.className += ' ' + this.style.deleted; + ret.props.__MLV2_deleteTime = record.delete_data.time; + }) + ); + const Message = ZeresPluginLibrary.WebpackModules.getByIndex(ZeresPluginLibrary.WebpackModules.getIndex(m => m.default && (m.default.displayName === 'Message' || (m.default.__originalFunction && m.default.__originalFunction.displayName === 'Message')))); + if (Message) { + this.unpatches.push( + ZeresPluginLibrary.Patcher.after(this.getName(), Message, 'default', (_, [props], ret) => { + if (!props.__MLV2_deleteTime) return; + const oRef = ret.ref; + ret.ref = e => { + if (e && !e.__tooltip) { + new ZeresPluginLibrary.EmulatedTooltip(e, 'Deleted: ' + this.tools.createMomentObject(props.__MLV2_deleteTime).format('LLLL'), { side: 'left' }); + e.__tooltip = true; + } + if (typeof oRef === 'function') return oRef(e); + else if (XenoLib._.isObject(oRef)) oRef.current = e; + }; + }) + ); + } + this.forceReloadMessages(); + } + forceReloadMessages() { + const instance = ZeresPluginLibrary.Utilities.findInTree(ZeresPluginLibrary.ReactTools.getReactInstance(document.querySelector('.chat-3bRxxu .content-yTz4x3')), e => e && e.constructor && e.constructor.displayName === 'ChannelChat', { walkable: ['child', 'stateNode'] }); + if (!instance) return; + const unpatch = ZeresPluginLibrary.Patcher.after(this.getName() + '_RERENDER', instance, 'render', (_this, _, ret) => { + unpatch(); + if (!ret) return; + ret.key = ZeresPluginLibrary.DiscordModules.KeyGenerator(); + ret.ref = () => _this.forceUpdate(); + }); + instance.forceUpdate(); + } + patchModal() { + // REQUIRED not anymore I guess lol + try { + const confirmModal = ZeresPluginLibrary.WebpackModules.getByDisplayName('ConfirmModal'); + this.createModal.confirmationModal = props => { + try { + const ret = confirmModal(props); + if (props.size) ret.props.size = props.size; + + if (props.onCancel) { + const cancelButton = ZeresPluginLibrary.Utilities.findInReactTree(ret, e => e && e.type === XenoLib.ReactComponents.Button && e.props && e.props.look); + if (cancelButton) cancelButton.props.onClick = props.onCancel; + } + return ret; + } catch (err) { + if (props.onCancel) props.onCancel(); + else props.onClose(); + return null; + } + }; + this.createModal.confirmationModal.Sizes = ZeresPluginLibrary.WebpackModules.getByProps('ModalSize').ModalSize; + } catch { } + this.ModalStack = ZeresPluginLibrary.WebpackModules.getByProps('openModal', 'hasModalOpen'); + this._modalsApiUnsubcribe = (this.ModalStack.modalsApi || this.ModalStack.useModalsStore).subscribe(_ => { + if (this.menu.open && !this.ModalStack.hasModalOpen(this.style.menu)) { + this.menu.filter = ''; + this.menu.open = false; + this.menu.shownMessages = -1; + if (this.menu.messages) this.menu.messages.length = 0; + } + }); + /* + this.createModal.confirmationModal = class ConfirmationModal extends ZeresPluginLibrary.DiscordModules.ConfirmationModal { + constructor(props) { + super(props); + this._handleSubmit = this.handleSubmit.bind(this); + this._handleClose = this.handleClose.bind(this); + this.handleSubmit = this.handleSubmitEx.bind(this); + this.handleClose = this.handleCloseEx.bind(this); + } + handleSubmitEx(e) { + if (this.props.ml2Data) onClearLog(e); + else return this._handleSubmit(e); + } + handleCloseEx(e) { + if (this.props.ml2Data) onChangeOrder(e); + else return this._handleClose(e); + } + render() { + const ret = super.render(); + if (!ret) return ret; + delete ret.props['aria-label']; + return ret; + } + }; + this.unpatches.push( + ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.DiscordModules.ConfirmationModal.prototype, 'componentDidMount', (thisObj, args, original) => { + if (thisObj.props.ml2Data) { + if (this.menu.refilterOnMount) { + this.refilterMessages(); + this.menu.refilterOnMount = false; + } + document.getElementById(this.style.menuMessages).parentElement.parentElement.parentElement.scrollTop = this.scrollPosition; + } + return original(...args); + }) + ); + */ + } + buildMenu(setup) { + const ret = ZeresPluginLibrary.DCM.buildMenu(setup); + return props => ret({ ...props, onClose: _ => { } }); + } + // >>-|| POPULATION ||-<< + createMessageGroup(message, isStart) { + let deleted = false; + let edited = false; + let details = 'Sent in'; + let channel = this.tools.getChannel(message.channel_id); + let timestamp = message.timestamp; + let author = this.tools.getUser(message.author.id); + let noUserInfo = false; + let userInfoBeingRequested = true; + const isBot = message.author.bot; + const record = this.messageRecord[message.id]; + if (record) { + deleted = !!record.delete_data; + edited = !!record.edit_history; + + if (deleted && edited) { + details = 'Edited and deleted from'; + timestamp = record.delete_data.time; + } else if (deleted) { + details = 'Deleted from'; + timestamp = record.delete_data.time; + } else if (edited) { + details = 'Last edit in'; // todo: purged? + if (typeof record.edit_history[record.edit_history.length - 1].time !== 'string') timestamp = record.edit_history[record.edit_history.length - 1].time; + } + } + + details += ` ${this.getLiteralName(message.guild_id || (channel && channel.guild_id), message.channel_id)} `; + + details += `at ${this.createTimeStamp(timestamp, true)}`; + + details = details.replace(/[<>"&]/g, c => ({ "<": "<", ">": ">", "\"": """, "&": "&" })[c]); + const classes = this.createMessageGroup.classes; + const getAvatarOf = user => { + if (!user.avatar) return '/assets/322c936a8c8be1b803cd94861bdfa868.png'; + return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=128`; + }; + if (!classes.extra) + classes.extra = [ + /* 0 */ XenoLib.joinClassNames(XenoLib.getClass('groupStart message'), XenoLib.getClass('groupStart cozyMessage'), XenoLib.getClass('systemMessage groupStart'), XenoLib.getClass('zalgo wrapper'), XenoLib.getClass('zalgo cozy'), XenoLib.getClass('cozy zalgo')), + /* 1 */ XenoLib.joinClassNames(XenoLib.getClass('groupStart message'), XenoLib.getClass('groupStart cozyMessage'), XenoLib.getClass('zalgo wrapper'), XenoLib.getClass('zalgo cozy'), XenoLib.getClass('cozy zalgo')), + /* 2 */ XenoLib.getClass('username header'), + /* 3 */ XenoLib.joinClassNames(XenoLib.getClass('clickable avatar'), XenoLib.getClass('avatar clickable')), + /* 4 */ XenoLib.joinClassNames(XenoLib.getClass('timestampTooltip username'), XenoLib.getClass('avatar clickable')), + /* 5 */ XenoLib.getClass('separator timestamp'), + /* 6 */ XenoLib.joinClassNames(this.multiClasses.markup, XenoLib.getClass('buttonContainer markupRtl')), + /* 7 */ XenoLib.getClass('embedWrapper container'), + /* 8 */ XenoLib.joinClassNames(XenoLib.getClass('zalgo latin24CompactTimeStamp'), XenoLib.getClass('separator timestamp'), XenoLib.getClass('alt timestampVisibleOnHover'), XenoLib.getClass('timestampVisibleOnHover alt')), + /* 9 */ XenoLib.getClass('latin24CompactTimeStamp separator'), + /* 10 */ XenoLib.getSingleClass('timestampTooltip username'), + /* 11 */ XenoLib.getSingleClass('separator timestamp'), + /* 12 */ XenoLib.getClass('zalgo contents') + ]; + + const element = isStart + ? this.parseHTML(`
+
+

${message.author.username.replace(/[<>"]/g, c => ({ "<": "<", ">": ">", "\"": """ })[c])}${(isBot && `BOT`) || ''}${details}

+
+
+
+
`) + : this.parseHTML(`
+
+ + + [ + ${this.createTimeStamp(timestamp, -1)} + ] + + +
+
+
+
`); + element.messageId = message.id; + const profImg = element.getElementsByClassName(classes.avatarImgSingle)[0]; + if (profImg) { + profImg.onerror = () => { + profImg.src = '/assets/322c936a8c8be1b803cd94861bdfa868.png'; + }; + const verifyProfilePicture = () => { + if (message.author.avatar != author.avatar && author.avatar) { + profImg.src = getAvatarOf(author); + if (record) { + record.message.author.avatar = author.avatar; + } + } else { + if (record) record.message.author.avatar = null; + } + }; + if (!isBot || true) { + if (!author) { + author = message.author; + if (this.menu.userRequestQueue.findIndex(m => m.id === author.id) == -1) { + this.menu.userRequestQueue.push({ + id: author.id, + username: author.username, + success: [ + res => { + author = $.extend(true, {}, res); + verifyProfilePicture(); + userInfoBeingRequested = false; + } + ], + fail: [ + () => { + noUserInfo = true; + userInfoBeingRequested = false; + } + ] + }); + } else { + const dt = this.menu.userRequestQueue.find(m => m.id === author.id); + dt.success.push(res => { + author = $.extend(true, {}, res); + verifyProfilePicture(); + userInfoBeingRequested = false; + }); + dt.fail.push(() => { + noUserInfo = true; + userInfoBeingRequested = false; + }); + } + } else { + userInfoBeingRequested = false; + verifyProfilePicture(); + } + } + const profIcon = element.getElementsByClassName(classes.avatarImgSingle)[0]; + profIcon.addEventListener('click', () => { + //if (isBot) return this.showToast('User is a bot, this action is not possible on a bot.', { type: 'error', timeout: 5000 }); + if (userInfoBeingRequested) return this.showToast('Internal error', { type: 'info', timeout: 5000 }); + if (noUserInfo) return this.showToast('Could not get user info!', { type: 'error' }); + ZeresPluginLibrary.Popouts.showUserPopout(profIcon, author); + }); + profIcon.addEventListener('contextmenu', e => { + //if (isBot) return this.showToast('User is a bot, this action is not possible on a bot.', { type: 'error', timeout: 5000 }); + if (userInfoBeingRequested) return this.showToast('Internal error', { type: 'info', timeout: 5000 }); + if (noUserInfo) return this.showToast('Could not get user info! You can only delete or copy to clipboard!', { timeout: 5000 }); + ZeresPluginLibrary.WebpackModules.getByProps('openUserContextMenu').openUserContextMenu(e, author, channel || this.menu.randomValidChannel); + }); + const nameLink = element.getElementsByClassName(classes.extra[10])[0]; + nameLink.addEventListener('click', () => { + //if (isBot) return this.showToast('User is a bot, this action is not possible on a bot.', { type: 'error', timeout: 5000 }); + if (userInfoBeingRequested) return this.showToast('Internal error', { type: 'info', timeout: 5000 }); + if (noUserInfo) return this.showToast('Could not get user info!', { type: 'error' }); + ZeresPluginLibrary.Popouts.showUserPopout(nameLink, author); + }); + nameLink.addEventListener('contextmenu', e => { + //if (isBot) return this.showToast('User is a bot, this action is not possible on a bot.', { type: 'error', timeout: 5000 }); + if (userInfoBeingRequested) return this.showToast('Internal error', { type: 'info', timeout: 5000 }); + if (noUserInfo) return this.showToast('Could not get user info! You can only delete or copy to clipboard!', { type: 'error', timeout: 5000 }); + ZeresPluginLibrary.WebpackModules.getByProps('openUserContextMenu').openUserContextMenu(e, author, channel || this.menu.randomValidChannel); + }); + const timestampEl = element.getElementsByClassName(classes.extra[11])[0]; + timestampEl.addEventListener('contextmenu', e => { + const messages = [element]; + let target = element.nextElementSibling; + while (target && target.classList && !target.classList.contains(XenoLib.getSingleClass('systemMessage groupStart'))) { + messages.push(target); + target = target.nextElementSibling; + } + if (!messages.length) return; + const messageIds = []; + for (let i = 0; i < messages.length; i++) if (messages[i] && messages[i].messageId) messageIds.push(messages[i].messageId); + if (!messageIds.length) return; + ZeresPluginLibrary.DCM.openContextMenu( + e, + this.buildMenu([ + { + type: 'group', + items: [ + { + label: 'Copy Formatted Message', + action: () => { + ZeresPluginLibrary.DiscordModules.ContextMenuActions.closeContextMenu(); + let result = ''; + for (let msgid of messageIds) { + const record = this.messageRecord[msgid]; + if (!record) continue; + if (!result.length) result += `> **${record.message.author.username}** | ${this.createTimeStamp(record.message.timestamp, true)}\n`; + result += `> ${record.message.content.replace(/\n/g, '\n> ')}\n`; + } + this.nodeModules.electron.clipboard.writeText(result); + this.showToast('Copied!', { type: 'success' }); + } + }, + { + type: 'item', + label: 'Remove Group From Log', + action: () => { + ZeresPluginLibrary.DiscordModules.ContextMenuActions.closeContextMenu(); + let invalidatedChannelCache = false; + for (let msgid of messageIds) { + const record = this.messageRecord[msgid]; + if (!record) continue; // the hell + if ((record.edit_history && !record.edits_hidden) || (record.delete_data && !record.delete_data.hidden)) this.invalidateChannelCache((invalidatedChannelCache = record.message.channel_id)); + this.deleteMessageFromRecords(msgid); + } + if (invalidatedChannelCache) this.cacheChannelMessages(invalidatedChannelCache); + this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly + this.saveData(); + } + } + ] + } + ]) + ); + }); + timestampEl.addEventListener('click', e => { + if (!this.menu.deleteKeyDown) return; + const messages = [element]; + let target = element.nextElementSibling; + while (target && target.classList && !target.classList.contains(XenoLib.getSingleClass('systemMessage groupStart'))) { + messages.push(target); + target = target.nextElementSibling; + } + if (!messages.length) return; + const messageIds = []; + for (let i = 0; i < messages.length; i++) if (messages[i] && messages[i].messageId) messageIds.push(messages[i].messageId); + if (!messageIds.length) return; + let invalidatedChannelCache = false; + for (let msgid of messageIds) { + const record = this.messageRecord[msgid]; + if (!record) continue; // the hell + if ((record.edit_history && !record.edits_hidden) || (record.delete_data && !record.delete_data.hidden)) this.invalidateChannelCache((invalidatedChannelCache = record.message.channel_id)); + this.deleteMessageFromRecords(msgid); + } + if (invalidatedChannelCache) this.cacheChannelMessages(invalidatedChannelCache); + this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly + this.saveData(); + }); + new ZeresPluginLibrary.EmulatedTooltip(timestampEl, 'Sent at ' + this.tools.createMomentObject(message.timestamp).format('LLLL'), { side: 'top' }); + } + const messageContext = e => { + let target = e.target; + if (!target.classList.contains('mention') || (target.tagName == 'DIV' && target.classList.contains(ZeresPluginLibrary.WebpackModules.getByProps('imageError').imageError.split(/ /g)[0]))) { + let isMarkup = false; + let isEdited = false; + let isBadImage = target.tagName == 'DIV' && target.classList == ZeresPluginLibrary.WebpackModules.getByProps('imageError').imageError; + if (!isBadImage) { + while (target && (!target.classList || !(isMarkup = target.classList.contains(this.classes.markup)))) { + if (target.classList && target.classList.contains(this.style.edited)) isEdited = target; + target = target.parentElement; + } + } + + if (isMarkup || isBadImage) { + const messageId = message.id; + const record = this.getSavedMessage(messageId); + if (!record) return; + let editNum = -1; + if (isEdited) editNum = isEdited.edit; + const menuItems = []; + if (channel) { + menuItems.push({ + type: 'item', + label: 'Jump to Message', + action: () => { + ZeresPluginLibrary.DiscordModules.ContextMenuActions.closeContextMenu(); + this.jumpToMessage(message.channel_id, messageId, message.guild_id); + } + }); + } + if (!isBadImage || record.message.content.length) { + menuItems.push( + { + type: 'item', + label: 'Copy Text', + action: () => { + ZeresPluginLibrary.DiscordModules.ContextMenuActions.closeContextMenu(); + this.nodeModules.electron.clipboard.writeText(editNum != -1 ? record.edit_history[editNum].content : record.message.content); + this.showToast('Copied!', { type: 'success' }); + } + }, + { + type: 'item', + label: 'Copy Formatted Message', + action: () => { + ZeresPluginLibrary.DiscordModules.ContextMenuActions.closeContextMenu(); + const content = editNum != -1 ? record.edit_history[editNum].content : record.message.content; + const result = `> **${record.message.author.username}** | ${this.createTimeStamp(record.message.timestamp, true)}\n> ${content.replace(/\n/g, '\n> ')}`; + this.nodeModules.electron.clipboard.writeText(result); + this.showToast('Copied!', { type: 'success' }); + } + } + ); + } + if (record.delete_data && record.delete_data.hidden) { + menuItems.push({ + type: 'item', + label: 'Unhide Deleted Message', + action: () => { + ZeresPluginLibrary.DiscordModules.ContextMenuActions.closeContextMenu(); + record.delete_data.hidden = false; + this.invalidateChannelCache(record.message.channel_id); // good idea? + this.cacheChannelMessages(record.message.channel_id); + this.saveData(); + this.showToast('Unhidden!', { type: 'success' }); + } + }); + } + if (record.edit_history) { + if (editNum != -1) { + menuItems.push({ + type: 'item', + label: 'Delete Edit', + action: () => { + ZeresPluginLibrary.DiscordModules.ContextMenuActions.closeContextMenu(); + this.deleteEditedMessageFromRecord(messageId, editNum); + this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly + this.showToast('Deleted!', { type: 'success' }); + } + }); + } + if (record.edits_hidden) { + menuItems.push({ + type: 'item', + label: 'Unhide Edits', + action: () => { + ZeresPluginLibrary.DiscordModules.ContextMenuActions.closeContextMenu(); + record.edits_hidden = false; + this.saveData(); + this.showToast('Unhidden!', { type: 'success' }); + } + }); + } + } + menuItems.push( + { + type: 'item', + label: 'Remove From Log', + action: () => { + ZeresPluginLibrary.DiscordModules.ContextMenuActions.closeContextMenu(); + let invalidatedChannelCache = false; + if ((record.edit_history && !record.edits_hidden) || (record.delete_data && !record.delete_data.hidden)) this.invalidateChannelCache((invalidatedChannelCache = record.message.channel_id)); + this.deleteMessageFromRecords(messageId); + this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly + if (invalidatedChannelCache) this.cacheChannelMessages(invalidatedChannelCache); + this.saveData(); + if (record.message.channel_id !== this.selectedChannel.id) return; + if (record.delete_data) { + ZeresPluginLibrary.DiscordModules.Dispatcher.dirtyDispatch({ + type: 'MESSAGE_DELETE', + id: messageId, + channelId: record.message.channel_id, + ML2: true // ignore ourselves lol, it's already deleted + // on a side note, probably does nothing if we don't ignore + }); + } else { + ZeresPluginLibrary.DiscordModules.Dispatcher.dirtyDispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT', id: messageId }); + } + } + }, + { + type: 'item', + label: 'Copy Message ID', + action: () => { + ZeresPluginLibrary.DiscordModules.ContextMenuActions.closeContextMenu(); + this.nodeModules.electron.clipboard.writeText(messageId); // todo: store electron or writeText somewhere? + this.showToast('Copied!', { type: 'success' }); + } + } + ); + ZeresPluginLibrary.DCM.openContextMenu( + e, + this.buildMenu([ + { + type: 'group', + items: menuItems + } + ]) + ); + return; + } + } + }; + element.addEventListener('contextmenu', e => messageContext(e)); + element.addEventListener('click', e => { + if (!this.menu.deleteKeyDown) return; + let target = e.target; + let isMarkup = false; + let isEdited = false; + let isBadImage = target.tagName == 'DIV' && target.classList == ZeresPluginLibrary.WebpackModules.getByProps('imageError').imageError; + if (!isBadImage) { + while (!target.classList.contains('message-2qnXI6') && !(isMarkup = target.classList.contains(this.classes.markup))) { + if (target.classList.contains(this.style.edited)) isEdited = target; + target = target.parentElement; + } + } + if (!isMarkup && !isBadImage) return; + const messageId = message.id; + const record = this.messageRecord[messageId]; + if (!record) return; + this.invalidateChannelCache(record.message.channel_id); // good idea? + this.cacheChannelMessages(record.message.channel_id); + if (isEdited) { + this.deleteEditedMessageFromRecord(messageId, isEdited.edit); + } else { + this.deleteMessageFromRecords(messageId); + } + this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly + this.saveData(); + }); + return element; + } + populateParent(parent, messages) { + let lastMessage; + let lastType; /* unused */ + let messageGroup; + const populate = i => { + try { + // todo: maybe make the text red if it's deleted? + const messageId = messages[i]; + const record = this.getSavedMessage(messageId); + const message = record ? record.message : this.getMessageAny(messageId); + if (!message) return; + // todo: get type and use it + if (!messageGroup /* || !lastType */ || !lastMessage || lastMessage.channel_id != message.channel_id || lastMessage.author.id != message.author.id || new Date(message.timestamp).getDate() !== new Date(lastMessage.timestamp).getDate() || (message.attachments.length && message.content.length)) { + messageGroup = this.createMessageGroup(message, true); + } else { + messageGroup = this.createMessageGroup(message); + } + lastMessage = message; + const markup = messageGroup.getElementsByClassName(this.classes.markup)[0]; + const contentDiv = messageGroup.getElementsByClassName(XenoLib.getSingleClass('embedWrapper container'))[0]; + if (record && record.edit_history) { + markup.classList.add(this.style.edited); + for (let ii = 0; ii < record.edit_history.length; ii++) { + const hist = record.edit_history[ii]; + const editedMarkup = this.formatMarkup(hist.content, message.channel_id); + editedMarkup.insertAdjacentHTML('beforeend', ``); // TODO, change this + new ZeresPluginLibrary.EmulatedTooltip(editedMarkup, 'Edited at ' + (typeof hist.time === 'string' ? hist.time : this.createTimeStamp(hist.time)), { side: 'left' }); + editedMarkup.classList.add(this.style.edited); + editedMarkup.edit = ii; + markup.appendChild(editedMarkup); + } + } + markup.append(this.formatMarkup(message.content, message.channel_id)); + if (!record) { + const channel = this.tools.getChannel(message.channel_id); + const guild = this.tools.getServer(channel && channel.guild_id); + markup.addEventListener('click', () => this.jumpToMessage(message.channel_id, message.id, guild && guild.id)); + } + // todo, embeds + // how do I do embeds? + + // why don't attachments show for sent messages? what's up with that? + if (message.attachments.length) { + // const attachmentsContent = this.parseHTML(`
`); + const attemptToUseCachedImage = (attachmentId, attachmentIdx, hidden, filename, width, height) => { + const img = document.createElement('img'); + img.classList = ZeresPluginLibrary.WebpackModules.getByProps('clickable').clickable; + img.messageId = messageId; + img.idx = attachmentIdx; + img.id = attachmentId; // USED FOR FINDING THE IMAGE THRU CONTEXT MENUS + if (hidden) { + img.src = `https://i.clouds.tf/q2vy/r8q6.png#${record.message.channel_id},${img.id}`; + img.width = 200; + } else { + img.src = 'http://localhost:7474/' + attachmentId + filename.match(/\.[0-9a-z]+$/i)[0] + `#${record.message.channel_id},${img.id}`; + img.width = 256; + } + img.addEventListener('click', e => { + if (this.menu.deleteKeyDown) { + this.deleteMessageFromRecords(messageId); + this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly + this.saveData(); + return; + } + this.createModal( + { + src: img.src + '?ML2=true', // self identify + placeholder: img.src, // cute image here + original: img.src, + width: width, + height: height, + onClickUntrusted: e => e.openHref(), + className: this.style.imageRoot + }, + true + ); + }); + img.onerror = () => { + const imageErrorDiv = document.createElement('div'); + imageErrorDiv.classList = ZeresPluginLibrary.WebpackModules.getByProps('imageError').imageError; + imageErrorDiv.messageId = messageId; + contentDiv.replaceChild(imageErrorDiv, img); + }; + contentDiv.appendChild(img); + return true; + }; + const handleCreateImage = (attachment, idx) => { + if (attachment.url == 'ERROR') { + attemptToUseCachedImage(attachment.id, idx, attachment.hidden, attachment.filename, attachment.width, attachment.height); + } else { + if (!this.isImage(attachment.url)) return; // bruh + const img = document.createElement('img'); + img.classList = ZeresPluginLibrary.WebpackModules.getByProps('clickable').clickable; + img.messageId = messageId; + img.id = attachment.id; // USED FOR FINDING THE IMAGE THRU CONTEXT MENUS + img.idx = idx; + // img.style.minHeight = '104px'; // bruh? + if (record) { + img.addEventListener('click', () => { + if (this.menu.deleteKeyDown) { + this.deleteMessageFromRecords(messageId); + this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly + this.saveData(); + return; + } + this.createModal( + { + src: attachment.url + '?ML2=true', // self identify + placeholder: attachment.url, // cute image here + original: attachment.url, + width: attachment.width, + height: attachment.height, + onClickUntrusted: e => e.openHref(), + className: this.style.imageRoot + }, + true + ); + }); + } + img.onerror = () => { + if (img.triedCache) { + const imageErrorDiv = document.createElement('div'); + imageErrorDiv.classList = ZeresPluginLibrary.WebpackModules.getByProps('imageError').imageError; + imageErrorDiv.messageId = messageId; + contentDiv.replaceChild(imageErrorDiv, img); + } + if (record) { + this.nodeModules.request.head(attachment.url, (err, res) => { + if (err || res.statusCode != 404) return; + record.message.attachments[idx].url = 'ERROR'; + img.src = 'http://localhost:7474/' + attachment.id + attachment.filename.match(/\.[0-9a-z]+$/)[0]; + img.triedCache = true; + }); + } + }; + if (attachment.hidden) { + img.src = `https://i.clouds.tf/q2vy/r8q6.png#${record.message.channel_id},${img.id}`; + img.width = 200; + } else { + img.src = attachment.url; + img.width = this.clamp(attachment.width, 200, 650); + } + contentDiv.appendChild(img); + } + }; + for (let ii = 0; ii < message.attachments.length; ii++) handleCreateImage(message.attachments[ii], ii); + } + if (message.embeds && message.embeds.length && false) { + const ddiv = document.createElement('div'); + // TODO: optimize + if (!this.populateParent.__embedcontainer) this.populateParent.__embedcontainer = this.safeGetClass(() => ZeresPluginLibrary.WebpackModules.getByProps('containerCozy', 'gifFavoriteButton').containerCozy, 'containerCozy'); + ddiv.className = this.populateParent.__embedcontainer; + const fuckme = new (ZeresPluginLibrary.WebpackModules.getByDisplayName('MessageAccessories'))({ channel: this.tools.getChannel(message.channel_id) || this.menu.randomValidChannel }); + for (const embed of message.embeds) { + const embedBase = { + GIFVComponent: ZeresPluginLibrary.WebpackModules.getByDisplayName('LazyGIFV'), + ImageComponent: ZeresPluginLibrary.WebpackModules.getByDisplayName('LazyImageZoomable'), + LinkComponent: ZeresPluginLibrary.WebpackModules.getByDisplayName('MaskedLink'), + VideoComponent: ZeresPluginLibrary.WebpackModules.getByDisplayName('LazyVideo'), + allowFullScreen: true, + autoPlayGif: true, + backgroundOpacity: '', + className: ZeresPluginLibrary.WebpackModules.getByProps('embedWrapper', 'gifFavoriteButton').embedWrapper, + embed: ZeresPluginLibrary.WebpackModules.getByProps('sanitizeEmbed').sanitizeEmbed(message.channel_id, message.id, embed), + hideMedia: false, + inlineGIFV: true, + maxMediaHeight: 300, + maxMediaWidth: 400, + maxThumbnailHeight: 80, + maxThumbnailWidth: 80, + suppressEmbed: false, + renderTitle: fuckme.renderEmbedTitle.bind(fuckme), + renderDescription: fuckme.renderEmbedDescription.bind(fuckme), + renderLinkComponent: ZeresPluginLibrary.WebpackModules.getByProps('defaultRenderLinkComponent').defaultRenderLinkComponent, + renderImageComponent: ZeresPluginLibrary.WebpackModules.getByProps('renderImageComponent').renderImageComponent, + renderVideoComponent: ZeresPluginLibrary.WebpackModules.getByProps('renderVideoComponent').renderVideoComponent, + renderAudioComponent: ZeresPluginLibrary.WebpackModules.getByProps('renderAudioComponent').renderAudioComponent, + renderMaskedLinkComponent: ZeresPluginLibrary.WebpackModules.getByProps('renderMaskedLinkComponent').renderMaskedLinkComponent + }; + ZeresPluginLibrary.DiscordModules.ReactDOM.render(ZeresPluginLibrary.DiscordModules.React.createElement(ZeresPluginLibrary.WebpackModules.getByDisplayName('Embed'), embedBase), ddiv); + } + contentDiv.appendChild(ddiv); + } + if (!contentDiv.childElementCount && !message.content.length) return; // don't bother + //messageContent.appendChild(divParent); + parent.appendChild(messageGroup); + } catch (err) { + ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Error in populateParent', err); + } + }; + let i = 0; + const addMore = () => { + for (let added = 0; i < messages.length && (added < this.settings.renderCap || (this.menu.shownMessages != -1 && i < this.menu.shownMessages)); i++, added++) populate(i); + handleMoreMessages(); + this.menu.shownMessages = i; + }; + const handleMoreMessages = () => { + if (i < messages.length) { + const div = document.createElement('div'); + const moreButton = this.createButton('LOAD MORE', function () { + this.parentElement.remove(); + addMore(); + }); + moreButton.style.width = '100%'; + moreButton.style.marginBottom = '20px'; + div.appendChild(moreButton); + parent.appendChild(div); + } + }; + + if (this.settings.renderCap) addMore(); + else for (; i < messages.length; i++) populate(i); + this.processUserRequestQueue(); + if (!messages.length) { + const strong = document.createElement('strong'); + strong.className = this.multiClasses.defaultColor; + strong.innerText = "Not to worry, the logger is not broken! There simply wasn't anything logged in the selected tab."; + parent.appendChild(strong); + } + } + // >>-|| FILTERING ||-<< + sortMessagesByAge(map) { + // sort direction: new - old + map.sort((a, b) => { + const recordA = this.messageRecord[a]; + const recordB = this.messageRecord[b]; + if (!recordA || !recordB) return 0; + let timeA = new Date(recordA.message.timestamp).getTime(); + let timeB = new Date(recordB.message.timestamp).getTime(); + if (recordA.edit_history && typeof recordA.edit_history[recordA.edit_history.length - 1].time !== 'string') timeA = recordA.edit_history[recordA.edit_history.length - 1].time; + if (recordB.edit_history && typeof recordB.edit_history[recordB.edit_history.length - 1].time !== 'string') timeB = recordB.edit_history[recordB.edit_history.length - 1].time; + if (recordA.delete_data && recordA.delete_data.time) timeA = recordA.delete_data.time; + if (recordB.delete_data && recordB.delete_data.time) timeB = recordB.delete_data.time; + return parseInt(timeB) - parseInt(timeA); + }); + } + getFilteredMessages() { + let messages = []; + + const pushIdsIntoMessages = map => { + for (let channel in map) { + for (let messageIdIDX in map[channel]) { + messages.push(map[channel][messageIdIDX]); + } + } + }; + const checkIsMentioned = map => { + for (let channel in map) { + for (let messageIdIDX in map[channel]) { + const messageId = map[channel][messageIdIDX]; + const record = this.getSavedMessage(messageId); + if (!record) continue; + if (record.ghost_pinged) { + messages.push(messageId); + } + } + } + }; + + if (this.menu.selectedTab == 'sent') { + for (let i of this.cachedMessageRecord) { + messages.push(i.id); + } + } + if (this.menu.selectedTab == 'edited') pushIdsIntoMessages(this.editedMessageRecord); + if (this.menu.selectedTab == 'deleted') pushIdsIntoMessages(this.deletedMessageRecord); + if (this.menu.selectedTab == 'purged') pushIdsIntoMessages(this.purgedMessageRecord); + if (this.menu.selectedTab == 'ghostpings') { + checkIsMentioned(this.deletedMessageRecord); + checkIsMentioned(this.editedMessageRecord); + checkIsMentioned(this.purgedMessageRecord); + } + + const filters = this.menu.filter.split(','); + + for (let i = 0; i < filters.length; i++) { + const split = filters[i].split(':'); + if (split.length < 2) continue; + + const filterType = split[0].trim().toLowerCase(); + const filter = split[1].trim().toLowerCase(); + + if (filterType == 'server' || filterType == 'guild') + messages = messages.filter(x => { + const message = this.getMessageAny(x); + if (!message) return false; + const channel = this.tools.getChannel(message.channel_id); + const guild = this.tools.getServer(message.guild_id || (channel && channel.guild_id)); + return (message.guild_id || (channel && channel.guild_id)) == filter || (guild && guild.name.toLowerCase().includes(filter.toLowerCase())); + }); + + if (filterType == 'channel') + messages = messages.filter(x => { + const message = this.getMessageAny(x); + if (!message) return false; + const channel = this.tools.getChannel(message.channel_id); + return message.channel_id == filter || (channel && channel.name.toLowerCase().includes(filter.replace('#', '').toLowerCase())); + }); + + if (filterType == 'message' || filterType == 'content') + messages = messages.filter(x => { + const message = this.getMessageAny(x); + return x == filter || (message && message.content.toLowerCase().includes(filter.toLowerCase())); + }); + + if (filterType == 'user') + messages = messages.filter(x => { + const message = this.getMessageAny(x); + if (!message) return false; + const channel = this.tools.getChannel(message.channel_id); + const member = ZeresPluginLibrary.DiscordModules.GuildMemberStore.getMember(message.guild_id || (channel && channel.guild_id), message.author.id); + return message.author.id == filter || message.author.username.toLowerCase().includes(filter.toLowerCase()) || (member && member.nick && member.nick.toLowerCase().includes(filter.toLowerCase())); + }); + + if (filterType == 'has') { + switch (filter) { + case 'image': + messages = messages.filter(x => { + const message = this.getMessageAny(x); + if (!message) return false; + if (Array.isArray(message.attachments)) if (message.attachments.some(({ filename }) => ZeresPluginLibrary.DiscordModules.DiscordConstants.IMAGE_RE.test(filename))) return true; + if (Array.isArray(message.embeds)) return message.embeds.some(({ image }) => !!image); + return false; + }); + break; + case 'link': + messages = messages.filter(x => { + const message = this.getMessageAny(x); + if (!message) return false; + return message.content.search(/https?:\/\/[\w\W]{2,}/) !== -1; + }); + break; + } + } + } + + if (this.menu.selectedTab != 'sent') { + this.sortMessagesByAge(messages); + if (this.settings.reverseOrder) messages.reverse(); // this gave me a virtual headache + } else if (!this.settings.reverseOrder) messages.reverse(); // this gave me a virtual headache + + return messages; + } + // >>-|| REPOPULATE ||-<< + refilterMessages() { + const messagesDIV = document.getElementById(this.style.menuMessages); + const original = messagesDIV.style.display; + messagesDIV.style.display = 'none'; + while (messagesDIV.firstChild) messagesDIV.removeChild(messagesDIV.firstChild); + this.menu.messages = this.getFilteredMessages(); + this.populateParent(messagesDIV, this.menu.messages); + messagesDIV.style.display = original; + } + // >>-|| HEADER ||-<< + openTab(tab) { + const tabBar = document.getElementById(this.style.menuTabBar); + if (!tabBar) return this.showToast(`Error switching to tab ${tab}!`, { type: 'error', timeout: 3000 }); + tabBar.querySelector(`.${this.style.tabSelected}`).classList.remove(this.style.tabSelected); + tabBar.querySelector('#' + tab).classList.add(this.style.tabSelected); + this.menu.selectedTab = tab; + setTimeout(() => this.refilterMessages(), 0); + } + createHeader() { + const classes = this.createHeader.classes; + const createTab = (title, id) => { + const tab = this.parseHTML(`
${title}
`); + tab.addEventListener('mousedown', () => this.openTab(id)); + return tab; + }; + const tabBar = this.parseHTML(`
`); + const tabs = tabBar.getElementsByClassName(classes.tabBarSingle)[0]; + tabs.appendChild(createTab('Sent', 'sent')); + tabs.appendChild(createTab('Deleted', 'deleted')); + tabs.appendChild(createTab('Edited', 'edited')); + tabs.appendChild(createTab('Purged', 'purged')); + tabs.appendChild(createTab('Ghost pings', 'ghostpings')); + const measureWidth = el => { + el = el.cloneNode(true); + + el.style.visibility = 'hidden'; + el.style.position = 'absolute'; + + document.body.appendChild(el); + let result = el.getBoundingClientRect().width; + el.remove(); + return result; + }; + const totalWidth = measureWidth(tabs) * 2 - 20; + const wantedTabWidth = totalWidth / tabs.childElementCount; + const wantedTabMargin = wantedTabWidth / 2; + let tab = tabs.firstElementChild; + while (tab) { + tab.style.marginRight = '0px'; + const tabWidth = measureWidth(tab); + if (tabWidth > wantedTabWidth) { + ZeresPluginLibrary.Logger.err(this.getName(), `What the shit? Tab ${tab} is massive!!`); + tab = tab.nextElementSibling; + continue; + } + tab.style.paddingRight = tab.style.paddingLeft = `${wantedTabMargin - tabWidth / 2}px`; + tab = tab.nextElementSibling; + } + tabBar.style.marginRight = '20px'; + return tabBar; + } + createTextBox() { + const classes = this.createTextBox.classes; + let textBox = this.parseHTML( + `
` + ); + const inputEl = textBox.getElementsByTagName('input')[0]; + inputEl.addEventListener('focusout', e => { + DOMTokenList.prototype.remove.apply(e.target.parentElement.parentElement.classList, classes.focused); + }); + inputEl.addEventListener('focusin', e => { + DOMTokenList.prototype.add.apply(e.target.parentElement.parentElement.classList, classes.focused); + }); + const onUpdate = e => { + if (this.menu.filterSetTimeout) clearTimeout(this.menu.filterSetTimeout); + this.menu.filter = inputEl.value; + const filters = this.menu.filter.split(','); + // console.log(filters); + if (!filters[0].length) return this.refilterMessages(); + this.menu.filterSetTimeout = setTimeout(() => { + if (filters[0].length) { + for (let i = 0; i < filters.length; i++) { + const split = filters[i].split(':'); + if (split.length < 2) return; + } + } + this.refilterMessages(); + }, 200); + }; + inputEl.addEventListener('keyup', onUpdate); // maybe I can actually use keydown but it didn't work for me + inputEl.addEventListener('paste', onUpdate); + const helpButton = textBox.getElementsByClassName(classes.questionMarkSingle)[0]; + helpButton.addEventListener('click', () => { + const extraHelp = this.createButton('Logger help', () => this.showLoggerHelpModal()); + this.createModal({ + confirmText: 'OK', + header: 'Filter help', + size: this.createModal.confirmationModal.Sizes.LARGE, + children: [ + ZeresPluginLibrary.ReactTools.createWrappedElement([ + this.parseHTML( + `
"server: " - Filter results with the specified server name or id. + "channel: " - Filter results with the specified channel name or id. + "user: " - Filter results with the specified username, nickname or userid. + "message: " or "content: " - Filter results with the specified message content. + "has: - Filter results to only images or links + + Separate the search tags with commas. + Example: server: tom's bd stuff, message: heck + + + Shortcut help: + + "Ctrl + M" (default) - Open message log. + "Ctrl + N" (default) - Open message log with selected channel filtered.\n\n
`.replace(/\n/g, '
') + ), + extraHelp + ]) + ], + red: false + }); + }); + new ZeresPluginLibrary.EmulatedTooltip(helpButton, 'Help!', { side: 'top' }); + return textBox; + } + // >>-|| MENU MODAL CREATION ||-<< + openWindow(type) { + if (this.menu.open) { + this.menu.scrollPosition = 0; + if (type) this.openTab(type); + return; + } + this.menu.open = true; + if (type) this.menu.selectedTab = type; + if (!this.menu.selectedTab) this.menu.selectedTab = 'deleted'; + const messagesDIV = this.parseHTML(`
`); + const viewportHeight = document.getElementById('app-mount').getBoundingClientRect().height; + messagesDIV.style.minHeight = viewportHeight * 0.514090909 + 'px'; // hack but ok + //messagesDIV.style.display = 'none'; + const onChangeOrder = el => { + this.settings.reverseOrder = !this.settings.reverseOrder; + el.target.innerText = 'Sort direction: ' + (!this.settings.reverseOrder ? 'new - old' : 'old - new'); // maybe a func? + this.saveSettings(); + this.refilterMessages(); + }; + + const Text = ZeresPluginLibrary.WebpackModules.getByDisplayName('Text'); + const onClearLog = e => { + if (!Text) return; + if (document.getElementById(this.style.filter).parentElement.parentElement.className.indexOf(this.createTextBox.classes.focused[0]) != -1) return; + let type = this.menu.selectedTab; + if (type === 'ghostpings') type = 'ghost pings'; + else { + type += ' messages'; + } + this.createModal({ + header: 'Clear log', + children: ZeresPluginLibrary.DiscordModules.React.createElement(Text, { size: Text.Sizes.SIZE_16, children: [`Are you sure you want to delete all ${type}${this.menu.filter.length ? ' that also match filter' : ''}?`] }), + confirmText: 'Confirm', + cancelText: 'Cancel', + onConfirm: () => { + if (this.menu.selectedTab == 'sent') { + if (!this.menu.filter.length) + for (let id of this.menu.messages) + this.cachedMessageRecord.splice( + this.cachedMessageRecord.findIndex(m => m.id === id), + 1 + ); + else this.cachedMessageRecord.length = 0; // hack, does it cause a memory leak? + } else { + for (let id of this.menu.messages) { + const record = this.messageRecord[id]; + let isSelected = false; + if (record) { + this.invalidateChannelCache(record.message.channel_id); + if (this.selectedChannel) isSelected = record.message.channel_id === this.selectedChannel.id; + } + this.deleteMessageFromRecords(id); + if (this.selectedChannel && isSelected) this.cacheChannelMessages(this.selectedChannel.id); + } + this.saveData(); + } + setImmediate(_ => this.refilterMessages()); + // this.menu.refilterOnMount = true; + } + }); + }; + this.createModal( + { + confirmText: 'Clear log', + cancelText: 'Sort direction: ' + (!this.settings.reverseOrder ? 'new - old' : 'old - new'), + header: ZeresPluginLibrary.ReactTools.createWrappedElement([this.createTextBox(), this.createHeader()]), + size: this.createModal.confirmationModal.Sizes.LARGE, + children: [ZeresPluginLibrary.ReactTools.createWrappedElement([messagesDIV])], + onCancel: onChangeOrder, + onConfirm: onClearLog, + onClose: _ => { }, + ml2Data: true, + className: this.style.menuRoot, + ref: e => { + if (!e) return; + /* advanced tech! */ + const stateNode = ZeresPluginLibrary.Utilities.getNestedProp(e, '_reactInternalFiber.return.return.stateNode.firstChild.childNodes.1.firstChild'); + if (!stateNode) return; + stateNode.addEventListener( + 'scroll', + this.tools.DiscordUtils.debounce(() => { + this.scrollPosition = document.getElementById(this.style.menuMessages).parentElement.parentElement.parentElement.scrollTop; + }, 100) + ); + } + }, + false, + this.style.menu + ); + let loadAttempts = 0; + const loadMessages = () => { + loadAttempts++; + try { + this.refilterMessages(); + } catch (e) { + if (loadAttempts > 4) { + XenoLib.Notifications.error(`Couldn't load menu messages! Report this issue to Lighty, error info is in console`, { timeout: 0 }); + ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Failed loading menu', e); + return; + } + setTimeout(loadMessages, 100); + } + }; + setTimeout(loadMessages, 100); + } + /* ==================================================-|| END MENU ||-================================================== */ + /* ==================================================-|| START CONTEXT MENU ||-================================================== */ + patchContextMenus() { + const Patcher = XenoLib.createSmartPatcher({ before: (moduleToPatch, functionName, callback, options = {}) => ZeresPluginLibrary.Patcher.before(this.getName(), moduleToPatch, functionName, callback, options), instead: (moduleToPatch, functionName, callback, options = {}) => ZeresPluginLibrary.Patcher.instead(this.getName(), moduleToPatch, functionName, callback, options), after: (moduleToPatch, functionName, callback, options = {}) => ZeresPluginLibrary.Patcher.after(this.getName(), moduleToPatch, functionName, callback, options) }); + const WebpackModules = ZeresPluginLibrary.WebpackModules; + this.unpatches.push( + Patcher.after( + WebpackModules.find(({ default: defaul }) => defaul && defaul.displayName === 'NativeImageContextMenu'), + 'default', + (_, [props], ret) => { + const newItems = []; + if (!this.menu.open) return; + const menu = ZeresPluginLibrary.Utilities.getNestedProp( + ZeresPluginLibrary.Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'), + 'props.children' + ); + if (!Array.isArray(menu)) return; + const addElement = (label, callback, id, options = {}) => newItems.push(XenoLib.createContextMenuItem(label, callback, id, options)); + let matched; + let isCached = false; + if (!props.src) return; + if (props.src.startsWith('data:image/png')) { + const cut = props.src.substr(0, 100); + matched = cut.match(/;(\d+);(\d+);/); + isCached = true; + } else { + matched = props.src.match(/.*ments\/(\d+)\/(\d+)\//); + if (!matched) matched = props.src.match(/r8q6.png#(\d+),(\d+)/); + if (!matched) { + matched = props.src.match(/localhost:7474.*#(\d+),(\d+)/); + isCached = true; + } + } + if (!matched) return; + const channelId = matched[1]; + const attachmentId = matched[2]; + const element = document.getElementById(attachmentId); + if (!element) return; + const attachmentIdx = element.idx; + const record = this.getSavedMessage(element.messageId); + if (!record) return; + addElement( + 'Save to Folder', + () => { + const { dialog } = this.nodeModules.electron.remote; + dialog + .showSaveDialog({ + defaultPath: record.message.attachments[attachmentIdx].filename + }) + .then(({ filePath: dir }) => { + if (!dir) return; + const attemptToUseCached = () => { + const srcFile = `${this.settings.imageCacheDir}/${attachmentId}${record.message.attachments[attachmentIdx].filename.match(/\.[0-9a-z]+$/)[0]}`; + if (!this.nodeModules.fs.existsSync(srcFile)) return this.showToast('Image does not exist locally!', { type: 'error', timeout: 5000 }); + this.nodeModules.fs.copyFileSync(srcFile, dir); + this.showToast('Saved!', { type: 'success' }); + }; + if (isCached) { + attemptToUseCached(); + } else { + const req = this.nodeModules.request(record.message.attachments[attachmentIdx].url); + req.on('response', res => { + if (res.statusCode == 200) { + req + .pipe(this.nodeModules.fs.createWriteStream(dir)) + .on('finish', () => this.showToast('Saved!', { type: 'success' })) + .on('error', () => this.showToast('Failed to save! No permissions.', { type: 'error', timeout: 5000 })); + } else if (res.statusCode == 404) { + attemptToUseCached(); + } else { + attemptToUseCached(); + } + }); + } + }); + }, + this.obfuscatedClass('save-to') + ); + addElement( + 'Copy to Clipboard', + () => { + const { clipboard, nativeImage } = this.nodeModules.electron; + const attemptToUseCached = () => { + const srcFile = `${this.settings.imageCacheDir}/${attachmentId}${record.message.attachments[attachmentIdx].filename.match(/\.[0-9a-z]+$/)[0]}`; + if (!this.nodeModules.fs.existsSync(srcFile)) return this.showToast('Image does not exist locally!', { type: 'error', timeout: 5000 }); + clipboard.write({ image: srcFile }); + this.showToast('Copied!', { type: 'success' }); + }; + if (isCached) { + attemptToUseCached(); + } else { + const path = this.nodeModules.path; + const process = require('process'); + // ImageToClipboard by Zerebos + this.nodeModules.request({ url: record.message.attachments[attachmentIdx].url, encoding: null }, (error, response, buffer) => { + if (error || response.statusCode != 200) { + this.showToast('Failed to copy. Image may not exist. Attempting to use local image cache.', { type: 'error' }); + attemptToUseCached(); + return; + } + if (process.platform === 'win32' || process.platform === 'darwin') { + clipboard.write({ image: nativeImage.createFromBuffer(buffer) }); + } else { + const file = path.join(process.env.HOME, 'ml2temp.png'); + this.nodeModules.fs.writeFileSync(file, buffer, { encoding: null }); + clipboard.write({ image: file }); + this.nodeModules.fs.unlinkSync(file); + } + this.showToast('Copied!', { type: 'success' }); + }); + } + }, + this.obfuscatedClass('copy-to') + ); + addElement( + 'Jump to Message', + () => { + this.jumpToMessage(channelId, element.messageId, record.message.guild_id); + }, + this.obfuscatedClass('jump-to') + ); + if (record.delete_data && record.delete_data.hidden) { + addElement( + 'Unhide Deleted Message', + () => { + record.delete_data.hidden = false; + this.invalidateChannelCache(record.message.channel_id); // good idea? + this.cacheChannelMessages(record.message.channel_id); + this.saveData(); + this.showToast('Unhidden!', { type: 'success' }); + }, + this.obfuscatedClass('unhide-deleted') + ); + } + if (record.edit_history && record.edits_hidden) { + addElement( + 'Unhide Message History', + () => { + record.edits_hidden = false; + this.invalidateChannelCache(record.message.channel_id); // good idea? + this.cacheChannelMessages(record.message.channel_id); + this.saveData(); + this.showToast('Unhidden!', { type: 'success' }); + }, + this.obfuscatedClass('unhide-edited') + ); + } + addElement( + 'Remove From Log', + () => { + this.deleteMessageFromRecords(element.messageId); + this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly + this.saveData(); + if (record.delete_data) ZeresPluginLibrary.DiscordModules.Dispatcher.dirtyDispatch({ type: 'MESSAGE_DELETE', id: messageId, channelId: channelId, ML2: true }); + else ZeresPluginLibrary.DiscordModules.Dispatcher.dirtyDispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT', id: messageId }); + }, + this.obfuscatedClass('remove') + ); + if (!props.src.startsWith('https://i.clouds.tf/q2vy/r8q6.png')) { + addElement( + 'Hide Image From Log', + () => { + record.message.attachments[attachmentIdx].hidden = true; + element.src = `https://i.clouds.tf/q2vy/r8q6.png#${channelId},${attachmentId}`; + element.width = 200; + }, + this.obfuscatedClass('hide-image') + ); + } else { + addElement( + 'Unhide Image From Log', + () => { + record.message.attachments[attachmentIdx].hidden = false; + const srcFile = `http://localhost:7474/${attachmentId}${record.message.attachments[attachmentIdx].filename.match(/\.[0-9a-z]+$/)[0]}#${channelId},${attachmentId}`; + element.src = record.message.attachments[attachmentIdx].url === 'ERROR' ? srcFile : record.message.attachments[attachmentIdx].url; + element.width = record.message.attachments[attachmentIdx].url === 'ERROR' ? 256 : this.clamp(record.message.attachments[attachmentIdx].width, 200, 650); + }, + this.obfuscatedClass('unhide-image') + ); + } + if (!newItems.length) return; + menu.push(XenoLib.createContextMenuGroup([XenoLib.createContextMenuSubMenu(this.settings.contextmenuSubmenuName, newItems, this.obfuscatedClass('mlv2'))])); + } + ) + ); + this.unpatches.push( + Patcher.after( + WebpackModules.find(({ default: defaul }) => defaul && defaul.displayName === 'MessageContextMenu'), + 'default', + (_, [props], ret) => { + const newItems = []; + const menu = ZeresPluginLibrary.Utilities.getNestedProp( + ZeresPluginLibrary.Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'), + 'props.children' + ); + if (!Array.isArray(menu)) return; + const addElement = (label, callback, id, options = {}) => newItems.push(XenoLib.createContextMenuItem(label, callback, id, options)); + addElement('Open Logs', () => this.openWindow(), this.obfuscatedClass('open')); + const messageId = props.message.id; + const channelId = props.channel.id; + const record = this.messageRecord[messageId]; + if (record) { + /* + addElement('Show in menu', () => { + this.menu.filter = `message:${messageId}`; + this.openWindow(); + }); */ + if (record.delete_data) { + const options = menu.find(m => m.props.children && m.props.children.length > 10); + options.props.children.splice(0, options.props.children.length); + addElement( + 'Hide Deleted Message', + () => { + ZeresPluginLibrary.DiscordModules.Dispatcher.dirtyDispatch({ + type: 'MESSAGE_DELETE', + id: messageId, + channelId: channelId, + ML2: true // ignore ourselves lol, it's already deleted + // on a side note, probably does nothing if we don't ignore + }); + this.showToast('Hidden!', { type: 'success' }); + record.delete_data.hidden = true; + this.saveData(); + }, + this.obfuscatedClass('hide-deleted') + ); + const idx = this.noTintIds.indexOf(messageId); + addElement( + `${idx !== -1 ? 'Add' : 'Remove'} Deleted Tint`, + () => { + if (idx !== -1) this.noTintIds.splice(idx, 1); + else this.noTintIds.push(messageId); + this.showToast(idx !== -1 ? 'Added!' : 'Removed!', { type: 'success' }); + }, + this.obfuscatedClass('change-tint') + ); + } + if (record.edit_history) { + if (record.edits_hidden) { + addElement( + 'Unhide Edits', + () => { + record.edits_hidden = false; + this.saveData(); + ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT', id: messageId }); + }, + this.obfuscatedClass('unhide-edits') + ); + } else { + let target = props.target; + if (target) { + while (target && target.className && target.className.indexOf(this.style.edited) === -1) { + target = target.parentElement; + } + if (target) { + if (!this.editModifiers[messageId]) { + addElement( + 'Hide Edits', + () => { + record.edits_hidden = true; + this.saveData(); + ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT', id: messageId }); + }, + this.obfuscatedClass('hide-edits') + ); + } + const editNum = target.getAttribute('editNum'); + if (this.editModifiers[messageId]) { + addElement( + `${this.editModifiers[messageId].noSuffix ? 'Show' : 'Hide'} (edited) Tag`, + () => { + this.editModifiers[messageId].noSuffix = true; + ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT', id: messageId }); + }, + this.obfuscatedClass('change-edit-tag') + ); + addElement( + `Undo Show As Message`, + () => { + delete this.editModifiers[messageId]; + ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT', id: messageId }); + }, + this.obfuscatedClass('undo-show-as-message') + ); + } else if (typeof editNum !== 'undefined' && editNum !== null) { + addElement( + 'Show Edit As Message', + () => { + this.editModifiers[messageId] = { editNum }; + ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT', id: messageId }); + }, + this.obfuscatedClass('show-as-message') + ); + addElement( + 'Delete Edit', + () => { + this.deleteEditedMessageFromRecord(messageId, parseInt(editNum)); + ZeresPluginLibrary.DiscordModules.Dispatcher.dispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT', id: messageId }); + }, + this.obfuscatedClass('delete-edit'), + { color: 'colorDanger' } + ); + } + } + } + } + } + if (record) { + addElement( + 'Remove From Log', + () => { + this.deleteMessageFromRecords(messageId); + this.saveData(); + if (record.delete_data) { + ZeresPluginLibrary.DiscordModules.Dispatcher.dirtyDispatch({ + type: 'MESSAGE_DELETE', + id: messageId, + channelId: channelId, + ML2: true // ignore ourselves lol, it's already deleted + // on a side note, probably does nothing if we don't ignore + }); + } else { + ZeresPluginLibrary.DiscordModules.Dispatcher.dirtyDispatch({ type: 'MLV2_FORCE_UPDATE_MESSAGE_CONTENT', id: messageId }); + } + }, + this.obfuscatedClass('remove-from-log'), + { color: 'colorDanger' } + ); + } + } + if (!newItems.length) return; + menu.push(XenoLib.createContextMenuGroup([XenoLib.createContextMenuSubMenu(this.settings.contextmenuSubmenuName, newItems, this.obfuscatedClass('mlv2'))])); + } + ) + ); + + const handleWhiteBlackList = (newItems, id) => { + const addElement = (label, callback, id, options = {}) => newItems.push(XenoLib.createContextMenuItem(label, callback, id, options)); + const whitelistIdx = this.settings.whitelist.findIndex(m => m === id); + const blacklistIdx = this.settings.blacklist.findIndex(m => m === id); + if (whitelistIdx == -1 && blacklistIdx == -1) { + addElement( + `Add to Whitelist`, + () => { + this.settings.whitelist.push(id); + this.saveSettings(); + this.showToast('Added!', { type: 'success' }); + }, + this.obfuscatedClass('add-whitelist') + ); + addElement( + `Add to Blacklist`, + () => { + this.settings.blacklist.push(id); + this.saveSettings(); + this.showToast('Added!', { type: 'success' }); + }, + this.obfuscatedClass('add-blacklist') + ); + } else if (whitelistIdx != -1) { + addElement( + `Remove From Whitelist`, + () => { + this.settings.whitelist.splice(whitelistIdx, 1); + this.saveSettings(); + this.showToast('Removed!', { type: 'success' }); + }, + this.obfuscatedClass('remove-whitelist') + ); + addElement( + `Move to Blacklist`, + () => { + this.settings.whitelist.splice(whitelistIdx, 1); + this.settings.blacklist.push(id); + this.saveSettings(); + this.showToast('Moved!', { type: 'success' }); + }, + this.obfuscatedClass('move-blacklist') + ); + } else { + addElement( + `Remove From Blacklist`, + () => { + this.settings.blacklist.splice(blacklistIdx, 1); + this.saveSettings(); + this.showToast('Removed!', { type: 'success' }); + }, + this.obfuscatedClass('remove-blacklist') + ); + addElement( + `Move to Whitelist`, + () => { + this.settings.blacklist.splice(blacklistIdx, 1); + this.settings.whitelist.push(id); + this.saveSettings(); + this.showToast('Moved!', { type: 'success' }); + }, + this.obfuscatedClass('move-whitelist') + ); + } + const notifIdx = this.settings.notificationBlacklist.indexOf(id); + addElement( + `${notifIdx === -1 ? 'Add To' : 'Remove From'} Notification Blacklist`, + () => { + if (notifIdx === -1) this.settings.notificationBlacklist.push(id); + else this.settings.notificationBlacklist.splice(notifIdx, 1); + this.saveSettings(); + this.showToast(notifIdx === -1 ? 'Added!' : 'Removed!', { type: 'success' }); + }, + this.obfuscatedClass('change-notif-blacklist') + ); + }; + + this.unpatches.push( + Patcher.after( + WebpackModules.find((e) => e && (e.default.displayName === 'ChannelListTextChannelContextMenu' && e.default.toString().search(/\(0,\w\.default\)\(\w,\w\),\w=\(0,\w\.default\)\(\w,\w\),\w=\(0,\w\.default\)\(\w,\w\),\w=\(0,\w\.default\)\(\w\),\w=\(0,\w\.default\)\(\w\.id\)/) !== -1 || e.__powercordOriginal_default.displayName === 'ChannelListTextChannelContextMenu' && e.__powercordOriginal_default.toString().search(/\(0,\w\.default\)\(\w,\w\),\w=\(0,\w\.default\)\(\w,\w\),\w=\(0,\w\.default\)\(\w,\w\),\w=\(0,\w\.default\)\(\w\),\w=\(0,\w\.default\)\(\w\.id\)/) !== -1)), + 'default', + (_, [props], ret) => { + const newItems = []; + const menu = ZeresPluginLibrary.Utilities.getNestedProp( + ZeresPluginLibrary.Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'), + 'props.children' + ); + if (!Array.isArray(menu)) return; + const addElement = (label, callback, id, options = {}) => newItems.push(XenoLib.createContextMenuItem(label, callback, id, options)); + addElement('Open Logs', () => this.openWindow(), this.obfuscatedClass('open')); + addElement( + `Open Log For Channel`, + () => { + this.menu.filter = `channel:${props.channel.id}`; + this.openWindow(); + }, + this.obfuscatedClass('open-channel') + ); + handleWhiteBlackList(newItems, props.channel.id); + if (!newItems.length) return; + menu.push(XenoLib.createContextMenuGroup([XenoLib.createContextMenuSubMenu(this.settings.contextmenuSubmenuName, newItems, this.obfuscatedClass('mlv2'))])); + } + ) + ); + + this.unpatches.push( + Patcher.after( + WebpackModules.find(({ default: defaul }) => defaul && defaul.displayName === 'GuildContextMenu'), + 'default', + (_, [props], ret) => { + const newItems = []; + const menu = ZeresPluginLibrary.Utilities.getNestedProp( + ZeresPluginLibrary.Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'), + 'props.children' + ); + if (!Array.isArray(menu)) return; + const addElement = (label, callback, id, options = {}) => newItems.push(XenoLib.createContextMenuItem(label, callback, id, options)); + addElement( + 'Open Logs', + () => { + this.openWindow(); + }, + this.obfuscatedClass('open') + ); + addElement( + `Open Log For Guild`, + () => { + this.menu.filter = `guild:${props.guild.id}`; + this.openWindow(); + }, + this.obfuscatedClass('open-guild') + ); + handleWhiteBlackList(newItems, props.guild.id); + if (!newItems.length) return; + menu.push(XenoLib.createContextMenuGroup([XenoLib.createContextMenuSubMenu(this.settings.contextmenuSubmenuName, newItems, this.obfuscatedClass('mlv2'))])); + } + ) + ); + + this.unpatches.push( + Patcher.after( + WebpackModules.find(({ default: defaul }) => defaul && defaul.displayName === 'GuildChannelUserContextMenu'), + 'default', + (_, [props], ret) => { + const newItems = []; + const menu = ZeresPluginLibrary.Utilities.getNestedProp( + ZeresPluginLibrary.Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'), + 'props.children' + ); + if (!Array.isArray(menu)) return; + const addElement = (label, callback, id, options = {}) => newItems.push(XenoLib.createContextMenuItem(label, callback, id, options)); + addElement( + 'Open Logs', + () => { + this.openWindow(); + }, + this.obfuscatedClass('open') + ); + addElement( + `Open Log For User`, + () => { + this.menu.filter = `user:${props.user.id}`; + this.openWindow(); + }, + this.obfuscatedClass('open-user') + ); + if (!newItems.length) return; + menu.push(XenoLib.createContextMenuGroup([XenoLib.createContextMenuSubMenu(this.settings.contextmenuSubmenuName, newItems, this.obfuscatedClass('mlv2'))])); + } + ) + ); + + this.unpatches.push( + Patcher.after( + WebpackModules.find(({ default: defaul }) => defaul && defaul.displayName === 'DMUserContextMenu'), + 'default', + (_, [props], ret) => { + const newItems = []; + const menu = ZeresPluginLibrary.Utilities.getNestedProp( + ZeresPluginLibrary.Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'), + 'props.children' + ); + if (!Array.isArray(menu)) return; + const addElement = (label, callback, id, options = {}) => newItems.push(XenoLib.createContextMenuItem(label, callback, id, options)); + addElement( + 'Open Logs', + () => { + this.openWindow(); + }, + this.obfuscatedClass('open') + ); + addElement( + `Open Log For User`, + () => { + this.menu.filter = `user:${props.user.id}`; + this.openWindow(); + }, + this.obfuscatedClass('open-user') + ); + addElement( + `Open Log For DM`, + () => { + this.menu.filter = `channel:${props.channel.id}`; + this.openWindow(); + }, + this.obfuscatedClass('open-dm') + ); + handleWhiteBlackList(newItems, props.channel.id); + if (!newItems.length) return; + menu.push(XenoLib.createContextMenuGroup([XenoLib.createContextMenuSubMenu(this.settings.contextmenuSubmenuName, newItems, this.obfuscatedClass('mlv2'))])); + } + ) + ); + + this.unpatches.push( + Patcher.after( + WebpackModules.find(({ default: defaul }) => defaul && defaul.displayName === 'GroupDMContextMenu'), + 'default', + (_, [props], ret) => { + const newItems = []; + const menu = ZeresPluginLibrary.Utilities.getNestedProp( + ZeresPluginLibrary.Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'), + 'props.children' + ); + if (!Array.isArray(menu)) return; + const addElement = (label, callback, id, options = {}) => newItems.push(XenoLib.createContextMenuItem(label, callback, id, options)); + addElement('Open Logs', () => this.openWindow(), this.obfuscatedClass('open')); + addElement( + `Open Log For Channel`, + () => { + this.menu.filter = `channel:${props.channel.id}`; + this.openWindow(); + }, + this.obfuscatedClass('open-channel') + ); + handleWhiteBlackList(newItems, props.channel.id); + if (!newItems.length) return; + menu.push(XenoLib.createContextMenuGroup([XenoLib.createContextMenuSubMenu(this.settings.contextmenuSubmenuName, newItems, this.obfuscatedClass('mlv2'))])); + } + ) + ); + } + /* ==================================================-|| END CONTEXT MENU ||-================================================== */ + }; + /*@end @*/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d509ecd --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Lightcock Plugins + +Plugins for lightcord :D + +All credits goes to `Lighty` for creating this plugins. +# +All plugins already removed Anti-Lightcord settings. \ No newline at end of file diff --git a/UnreadBadgesRedux.plugin.js b/UnreadBadgesRedux.plugin.js new file mode 100644 index 0000000..6f06693 --- /dev/null +++ b/UnreadBadgesRedux.plugin.js @@ -0,0 +1,609 @@ +//META{"name":"UnreadBadgesRedux","source":"https://gitdab.com/hana/lightcock/src/branch/master/UnreadBadgesRedux.plugin.js/","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=UnreadBadgesRedux","authorId":"287977955240706060","invite":"9jDgyVjTVX","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.\nJust go to settings, plugins and enable me.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!\nJust go to settings, plugins and enable me!', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); +@else@*/ +/* + * Copyright © 2019-2020, _Lighty_ + * All rights reserved. + * Code may not be redistributed, modified or otherwise taken without explicit permission. + */ +module.exports = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'UnreadBadgesRedux', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: 'LightyPon', + twitter_username: '' + } + ], + version: '1.0.10', + description: 'Adds a number badge to server icons and channels.', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/UnreadBadgesRedux/UnreadBadgesRedux.plugin.js' + }, + changelog: [ + { + title: 'fixed', + type: 'fixed', + items: ['Fixed not working on folders.'] + } + ], + defaultConfig: [ + { + type: 'category', + id: 'misc', + name: 'Display settings', + collapsible: true, + shown: true, + settings: [ + { + name: 'Display badge on folders', + id: 'folders', + type: 'switch', + value: true + }, + { + name: 'Ignore muted servers in folders unread badge count', + id: 'noMutedGuildsInFolderCount', + type: 'switch', + value: true + }, + { + name: 'Ignore muted channels in servers in folders unread badge count', + id: 'noMutedChannelsInGuildsInFolderCount', + type: 'switch', + value: true + }, + { + name: "Don't display badge on expanded folders", + id: 'expandedFolders', + type: 'switch', + value: true + }, + { + name: 'Display badge on servers', + id: 'guilds', + type: 'switch', + value: true + }, + { + name: 'Display badge on muted servers', + id: 'mutedGuilds', + type: 'switch', + value: true + }, + { + name: 'Ignore muted channels in server unread badge count', + id: 'noMutedInGuildCount', + type: 'switch', + value: true + }, + { + name: 'Display badge on channels', + id: 'channels', + type: 'switch', + value: true + }, + { + name: 'Display badge on muted channels', + id: 'mutedChannels', + type: 'switch', + value: true + }, + { + name: 'Display badge on left side on channels', + note: "In case you want the settings button to stay where it always is. This however doesn't move it before the NSFW tag if you use the BetterNsfwTag plugin", + id: 'channelsDisplayOnLeft', + type: 'switch', + value: false + }, + { + name: 'Background color', + id: 'backgroundColor', + type: 'color', + value: '#7289da', + options: { + defaultColor: '#7289da' + } + }, + { + name: 'Text color', + id: 'textColor', + type: 'color', + value: '#ffffff', + options: { + defaultColor: '#ffffff' + } + }, + { + name: 'Muted channel badge darkness', + id: 'mutedChannelBadgeDarkness', + type: 'slider', + value: 0.25, + min: 0, + max: 1, + equidistant: true, + options: { + equidistant: true + } + } + ] + } + ] + }; + + /* Build */ + const buildPlugin = ([Plugin, Api], BasePlugin) => { + const { Settings, Utilities, WebpackModules, DiscordModules, ColorConverter, ReactComponents, Patcher, PluginUtilities, Logger, ReactTools, ModalStack } = Api; + const { React, ChannelStore } = DiscordModules; + + const ReactSpring = WebpackModules.getByProps('useTransition'); + const BadgesModule = WebpackModules.getByProps('NumberBadge'); + const StoresModule = WebpackModules.getByProps('useStateFromStores'); + + /* discord won't let me access it, so I remade it :( */ + class BadgeContainer extends React.PureComponent { + componentDidMount() { + this.forceUpdate(); + } + componentWillAppear(e) { + e(); + } + componentWillEnter(e) { + e(); + } + componentWillLeave(e) { + this.timeoutId = setTimeout(e, 300); + } + componentWillUnmount() { + clearTimeout(this.timeoutId); + } + render() { + return React.createElement( + ReactSpring.animated.div, + { + className: this.props.className, + style: this.props.animatedStyle + }, + this.props.children + ); + } + } + + const UnreadStore = WebpackModules.getByProps('getUnreadCount'); + const MuteModule = WebpackModules.getByProps('isMuted'); + const AltChannelStore = WebpackModules.find(m => m.getChannels && m.getChannels.length === 1); + + const getUnreadCount = (guildId, includeMuted) => { + const channels = AltChannelStore.getChannels(guildId); + let count = 0; + for (const { channel } of channels.SELECTABLE) { + /* isChannelMuted is SLOW! */ + if (includeMuted || (!MuteModule.isChannelMuted(channel.guild_id, channel.id) && (!channel.parent_id || !MuteModule.isChannelMuted(channel.guild_id, channel.parent_id)))) count += UnreadStore.getUnreadCount(channel.id); + } + return count; + }; + + class Slider extends Settings.SettingField { + /* ripped out of ZeresPluginLibrary, because it does thingsin a way I DISLIKE! + but otherwise full credits to Zerebos + https://github.com/rauenzi/BDPluginLibrary/blob/master/src/ui/settings/types/slider.js + */ + constructor(name, note, min, max, value, onChange, options = {}) { + const props = { + onChange: _ => _, + defaultValue: value, + disabled: options.disabled ? true : false, + minValue: min, + maxValue: max, + handleSize: 10, + initialValue: value /* added this */ + }; + if (options.fillStyles) props.fillStyles = options.fillStyles; + if (options.markers) props.markers = options.markers; + if (options.stickToMarkers) props.stickToMarkers = options.stickToMarkers; + if (typeof options.equidistant != 'undefined') props.equidistant = options.equidistant; + super(name, note, onChange, DiscordModules.Slider, Object.assign(props, { onValueChange: v => this.onChange(v) })); + } + } + + return class UnreadBadgesRedux extends BasePlugin(Plugin) { + constructor() { + super(); + XenoLib.changeName(__filename, 'UnreadBadgesRedux'); + const oOnStart = this.onStart.bind(this); + this.onStart = () => { + try { + oOnStart(); + } catch (e) { + Logger.stacktrace('Failed to start!', e); + PluginUpdater.checkForUpdate(this.name, this.version, this._config.info.github_raw); + XenoLib.Notifications.error(`[**${this.name}**] Failed to start! Please update it, press CTRL + R, or ${GuildStore.getGuild(XenoLib.supportServerId) ? 'go to <#639665366380838924>' : '[join my support server](https://discord.gg/NYvWdN5)'} for further assistance.`, { timeout: 0 }); + try { + this.onStop(); + } catch (e) { } + } + }; + try { + WebpackModules.getByProps('openModal', 'hasModalOpen').closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + } + onStart() { + if (window.Lightcock) XenoLib.Notifications.warning(`[${this.getName()}] Lightcord is an unofficial and unsafe client with stolen code that is falsely advertising that it is safe, Lightcord has allowed the spread of token loggers hidden within plugins redistributed by them, and these plugins are not made to work on it. Your account is very likely compromised by malicious people redistributing other peoples plugins, especially if you didn't download this plugin from [GitHub](https://github.com/1Lighty/BetterDiscordPlugins/edit/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js), you should change your password immediately. Consider using a trusted client mod like [BandagedBD](https://rauenzi.github.io/BetterDiscordApp/) or [Powercord](https://powercord.dev/) to avoid losing your account.`, { timeout: 0 }); + this.promises = { state: { cancelled: false } }; + this.patchedModules = []; + this.patchAll(); + PluginUtilities.addStyle( + this.short + '-CSS', + ` + .unread-badge { + right: unset; + } + ` + ); + } + + onStop() { + this.promises.state.cancelled = true; + Patcher.unpatchAll(); + PluginUtilities.removeStyle(this.short + '-CSS'); + this.forceUpdateAll(); + } + + buildSetting(data) { + if (data.type === 'color') { + const setting = new XenoLib.Settings.ColorPicker(data.name, data.note, data.value, data.onChange, data.options); + if (data.id) setting.id = data.id; + return setting; + } else if (data.type === 'slider') { + const options = {}; + const { name, note, value, onChange, min, max } = data; + if (typeof data.markers !== 'undefined') options.markers = data.markers; + if (typeof data.stickToMarkers !== 'undefined') options.stickToMarkers = data.stickToMarkers; + const setting = new Slider(name, note, min, max, value, onChange, options); + if (data.id) setting.id = data.id; + return setting; + } + return super.buildSetting(data); + } + + saveSettings(_, setting, value) { + super.saveSettings(_, setting, value); + this.forceUpdateAll(); + } + + forceUpdateAll() { + this.patchedModules.forEach(e => e()); + } + + /* PATCHES */ + + patchAll() { + Utilities.suppressErrors(this.patchBlobMask.bind(this), 'BlobMask patch')(this.promises.state); + Utilities.suppressErrors(this.patchGuildIcon.bind(this), 'GuildIcon patch')(this.promises.state); + Utilities.suppressErrors(this.patchChannelItem.bind(this), 'ChannelItem patch')(this.promises.state); + Utilities.suppressErrors(this.patchConnectedGuild.bind(this), 'ConnectedGuild patch')(this.promises.state); + Utilities.suppressErrors(this.patchGuildFolder.bind(this), 'GuildFolder patch')(this.promises.state); + } + + async patchChannelItem(promiseState) { + const TextChannel = await ReactComponents.getComponentByName('TextChannel', `.${XenoLib.getSingleClass('mentionsBadge containerDefault')}`); + if (promiseState.cancelled) return; + const settings = this.settings; + const MentionsBadgeClassname = XenoLib.getClass('iconVisibility mentionsBadge'); + function UnreadBadge(e) { + const unreadCount = StoresModule.useStateFromStores([UnreadStore], () => { + if ((e.muted && !settings.misc.mutedChannels) || !settings.misc.channels) return 0; + const count = UnreadStore.getUnreadCount(e.channelId); + if (count > 1000) return Math.floor(count / 1000) * 1000; /* only trigger rerender if it changes in thousands */ + return count; + }); + if (!unreadCount) return null; + return React.createElement( + 'div', + { + className: MentionsBadgeClassname + }, + BadgesModule.NumberBadge({ count: unreadCount, color: e.muted ? ColorConverter.darkenColor(settings.misc.backgroundColor, settings.misc.mutedChannelBadgeDarkness * 100) : settings.misc.backgroundColor, style: { color: e.muted ? ColorConverter.darkenColor(settings.misc.textColor, settings.misc.mutedChannelBadgeDarkness * 100) : settings.misc.textColor } }) + ); + } + Patcher.after(TextChannel.component.prototype, 'render', (_this, _, ret) => { + const props = Utilities.findInReactTree(ret, e => e && Array.isArray(e.children) && e.children.find(e => e && e.type && e.type.displayName === 'ConnectedEditButton')); + if (!props || !props.children) return; + const badge = React.createElement(UnreadBadge, { channelId: _this.props.channel.id, muted: _this.props.muted && !_this.props.selected }); + props.children.splice(this.settings.misc.channelsDisplayOnLeft ? 0 : 2, 0, badge); + }); + TextChannel.forceUpdateAll(); + } + + async patchGuildFolder(promiseState) { + const settings = this.settings; + const FolderStore = WebpackModules.getByProps('isFolderExpanded'); + function BlobMaskWrapper(e) { + e.__UBR_unread_count = StoresModule.useStateFromStores([UnreadStore, MuteModule], () => { + if ((e.__UBR_folder_expanded && settings.misc.expandedFolders) || !settings.misc.folders) return 0; + let count = 0; + for (let i = 0; i < e.__UBR_guildIds.length; i++) { + const guildId = e.__UBR_guildIds[i]; + if (!settings.misc.noMutedGuildsInFolderCount || (settings.misc.noMutedGuildsInFolderCount && !MuteModule.isMuted(guildId))) count += getUnreadCount(guildId, !settings.misc.noMutedChannelsInGuildsInFolderCount); + } + if (count > 1000) return Math.floor(count / 1000) * 1000; /* only trigger rerender if it changes in thousands */ + return count; + }); + return React.createElement(e.__UBR_old_type, e); + } + BlobMaskWrapper.displayName = 'BlobMask'; + const GuildFolderMemo = WebpackModules.find(m => m.type && ((m.__powercordOriginal_type || m.type).toString().indexOf('.Messages.SERVER_FOLDER_PLACEHOLDER') !== -1 || (m.type.render && (m.type.__powercordOriginal_render || m.type.render).toString().indexOf('.Messages.SERVER_FOLDER_PLACEHOLDER') !== -1))); + Patcher.after(GuildFolderMemo.type.render ? GuildFolderMemo.type : GuildFolderMemo, GuildFolderMemo.type.render ? 'render' : 'type', (_, [props], ret) => { + const mask = Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'BlobMask'); + if (!mask) return; + mask.props.__UBR_old_type = mask.type; + mask.props.__UBR_guildIds = props.guildIds; + mask.props.__UBR_folder_expanded = FolderStore.isFolderExpanded(props.folderId); + mask.type = BlobMaskWrapper; + }); + const folders = [...document.querySelectorAll('.wrapper-21YSNc')].map(e => ReactTools.getOwnerInstance(e)); + folders.forEach(instance => { + if (!instance) return; + const unpatch = Patcher.after(instance, 'render', (_, __, ret) => { + unpatch(); + if (!ret) return; + ret.key = `GETGOOD${Math.random()}`; + const oRef = ret.props.setFolderRef; + ret.props.setFolderRef = (e, n) => { + _.forceUpdate(); + return oRef(e, n); + }; + }); + instance.forceUpdate(); + }); + } + + async patchConnectedGuild(promiseState) { + const selector = `.${XenoLib.getSingleClass('listItem', true)}`; + const ConnectedGuild = await ReactComponents.getComponentByName('DragSource(ConnectedGuild)', selector); + if (!ConnectedGuild.selector) ConnectedGuild.selector = selector; + if (promiseState.cancelled) return; + const settings = this.settings; + function PatchedConnectedGuild(e) { + /* get on my level scrublords */ + e.__UBR_unread_count = StoresModule.useStateFromStores([UnreadStore, MuteModule], () => (!settings.misc.guilds || (!settings.misc.mutedGuilds && MuteModule.isMuted(e.guildId)) ? 0 : getUnreadCount(e.guildId, !settings.misc.noMutedInGuildCount))); + return e.__UBR_old_type(e); + } + PatchedConnectedGuild.displayName = 'ConnectedGuild'; + Patcher.after(ConnectedGuild.component.prototype, 'render', (_this, _, ret) => { + const old = ret.props.children; + ret.props.children = e => { + const ret2 = old(e); + ret2.props.__UBR_old_type = ret2.type; + ret2.type = PatchedConnectedGuild; + return ret2; + }; + }); + ConnectedGuild.forceUpdateAll(); + this.patchedModules.push(ConnectedGuild.forceUpdateAll.bind(ConnectedGuild)); + } + + async patchGuildIcon(promiseState) { + const selector = `.${XenoLib.getSingleClass('listItem', true)}`; + const Guild = await ReactComponents.getComponentByName('Guild', selector); + if (!Guild.selector) Guild.selector = selector; + if (promiseState.cancelled) return; + Patcher.after(Guild.component.prototype, 'render', (_this, _, ret) => { + const mask = Utilities.findInTree(ret, e => e && e.type && e.type.displayName === 'BlobMask', { walkable: ['props', 'children'] }); + if (!mask) return; + mask.props.__UBR_unread_count = _this.props.__UBR_unread_count; + mask.props.guildId = _this.props.guildId; + }); + Guild.forceUpdateAll(); + } + + async patchBlobMask(promiseState) { + const selector = `.${XenoLib.getSingleClass('lowerBadge wrapper')}`; + const BlobMask = await ReactComponents.getComponentByName('BlobMask', selector); + if (!BlobMask.selector) BlobMask.selector = selector; + if (promiseState.cancelled) return; + const ensureUnreadBadgeMask = _this => { + if (_this.state.unreadBadgeMask) return; + _this.state.unreadBadgeMask = new ReactSpring.Controller({ + spring: 0 + }); + }; + Patcher.after(BlobMask.component.prototype, 'componentDidMount', _this => { + if (typeof _this.props.__UBR_unread_count !== 'number') return; + ensureUnreadBadgeMask(_this); + _this.state.unreadBadgeMask + .update({ + spring: !!_this.props.__UBR_unread_count, + immediate: true + }) + .start(); + }); + Patcher.after(BlobMask.component.prototype, 'componentWillUnmount', _this => { + if (typeof _this.props.__UBR_unread_count !== 'number') return; + if (!_this.state.unreadBadgeMask) return; + if (typeof _this.state.unreadBadgeMask.destroy === 'function') _this.state.unreadBadgeMask.destroy(); + else _this.state.unreadBadgeMask.dispose(); + _this.state.unreadBadgeMask = null; + }); + Patcher.after(BlobMask.component.prototype, 'componentDidUpdate', (_this, [{ __UBR_unread_count }]) => { + if (typeof _this.props.__UBR_unread_count !== 'number' || _this.props.__UBR_unread_count === __UBR_unread_count) return; + ensureUnreadBadgeMask(_this); + _this.state.unreadBadgeMask + .update({ + spring: !!_this.props.__UBR_unread_count, + immediate: !document.hasFocus(), + config: { + friction: 40, + tension: 900, + mass: 1 + } + }) + .start(); + }); + const LowerBadgeClassname = XenoLib.joinClassNames(XenoLib.getClass('wrapper lowerBadge'), 'unread-badge'); + Patcher.after(BlobMask.component.prototype, 'render', (_this, _, ret) => { + if (typeof _this.props.__UBR_unread_count !== 'number') return; + const badges = Utilities.findInTree(ret, e => e && e.type && e.type.displayName === 'TransitionGroup', { walkable: ['props', 'children'] }); + const masks = Utilities.findInTree(ret, e => e && e.type === 'mask', { walkable: ['props', 'children'] }); + if (!badges || !masks) return; + ensureUnreadBadgeMask(_this); + /* if count is 0, we're animating out, and as such, it's better to at least still display the old + count while animating out + */ + const counter = _this.props.__UBR_unread_count || _this.state.__UBR_old_unread_count; + if (_this.props.__UBR_unread_count) _this.state.__UBR_old_unread_count = _this.props.__UBR_unread_count; + const width = BadgesModule.getBadgeWidthForValue(counter); + const unreadCountMaskSpring = (_this.state.unreadBadgeMask.animated || _this.state.unreadBadgeMask.springs).spring; + masks.props.children.push( + React.createElement(ReactSpring.animated.rect, { + x: -4, + y: 28, + width: width + 8, + height: 24, + rx: 12, + ry: 12, + opacity: unreadCountMaskSpring.to([0, 0.5, 1], [0, 0, 1]), + transform: unreadCountMaskSpring.to([0, 1], [-16, 0]).to(e => `translate(${e} ${-e})`), + fill: 'black' + }) + ); + badges.props.children.unshift( + React.createElement( + BadgeContainer, + { + className: LowerBadgeClassname, + animatedStyle: { + opacity: unreadCountMaskSpring.to([0, 0.5, 1], [0, 0, 1]), + transform: unreadCountMaskSpring.to(e => `translate(${-20 + 20 * e} ${-1 * (16 - 16 * e)})`) + } + }, + React.createElement(BadgesModule.NumberBadge, { count: counter, color: this.settings.misc.backgroundColor, style: { color: this.settings.misc.textColor } }) + ) + ); + }); + BlobMask.forceUpdateAll(); + } + showChangelog = () => XenoLib.showChangelog(`${this.name} has been updated!`, this.version, this._config.changelog); + getSettingsPanel = () => + this.buildSettingsPanel() + .append(new XenoLib.Settings.PluginFooter(() => this.showChangelog())) + .getElement(); + }; + }; + + /* Finalize */ + + /* shared getters */ + const BasePlugin = cl => + class extends cl { + constructor() { + super(); + Object.defineProperties(this, { + name: { get: () => config.info.name }, + short: { get: () => config.info.name.split('').reduce((acc, char) => acc + (char === char.toUpperCase() ? char : '')) }, + author: { get: () => config.info.authors.map(author => author.name).join(', ') }, + version: { get: () => config.info.version }, + description: { get: () => config.info.description } + }); + } + }; + + /* this new lib loader is lit */ + let ZeresPluginLibraryOutdated = false; + let XenoLibOutdated = false; + try { + if (global.BdApi && 'function' == typeof BdApi.getPlugin) { + const a = (c, a) => ((c = c.split('.').map(b => parseInt(b))), (a = a.split('.').map(b => parseInt(b))), !!(a[0] > c[0])) || !!(a[0] == c[0] && a[1] > c[1]) || !!(a[0] == c[0] && a[1] == c[1] && a[2] > c[2]), + b = (b, c) => ((b && b._config && b._config.info && b._config.info.version && a(b._config.info.version, c)) || typeof global.isGay !== 'undefined'), + c = BdApi.getPlugin('ZeresPluginLibrary'), + d = BdApi.getPlugin('XenoLib'); + b(c, '1.2.26') && (ZeresPluginLibraryOutdated = !0), b(d, '1.3.31') && (XenoLibOutdated = !0); + } + } catch (a) { + console.error('Error checking if libraries are out of date', a); + } + + /* to anyone asking "why are you checking if x is out of date", well you see, sometimes, for whatever reason + the libraries are sometimes not updating for people. Either it doesn't check for an update, or the request + for some odd reason just fails. Yet, plugins update just fine with the same domain. + */ + return !global.ZeresPluginLibrary || !global.XenoLib || global.DiscordJSucks || ZeresPluginLibraryOutdated || XenoLibOutdated + ? class extends BasePlugin(class { }) { + constructor() { + super(); + this._XL_PLUGIN = true; + this.getName = () => this.name.replace(/\s+/g, ''); + this.getAuthor = () => this.author; + this.getVersion = () => this.version; + this.getDescription = () => this.description + (global.BetterDiscordConfig ? '' : ' You are missing libraries for this plugin, please enable the plugin and click Download Now.'); + this.start = this.load = this.handleMissingLib; + } + start() { } + stop() { } + handleMissingLib() { + if ("undefined" != typeof global.isGay) return; + const a = !!window.powercord && -1 !== (window.bdConfig && window.bdConfig.dataPath || "").indexOf("bdCompat") && "function" == typeof BdApi.__getPluginConfigPath, b = BdApi.findModuleByProps("openModal", "hasModalOpen"); + if (b && b.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const c = !global.XenoLib, d = !global.ZeresPluginLibrary, e = c && d || (c || d) && (XenoLibOutdated || ZeresPluginLibraryOutdated), + f = (() => { let a = ""; return c || d ? a += `Missing${XenoLibOutdated || ZeresPluginLibraryOutdated ? " and outdated" : ""} ` : (XenoLibOutdated || ZeresPluginLibraryOutdated) && (a += `Outdated `), a += `${e ? "Libraries" : "Library"} `, a })(), + g = (() => { + let a = `The ${e ? "libraries" : "library"} `; return c || XenoLibOutdated ? (a += "XenoLib ", (d || ZeresPluginLibraryOutdated) && (a += "and ZeresPluginLibrary ")) : (d || ZeresPluginLibraryOutdated) && (a += "ZeresPluginLibrary "), + a += `required for ${this.name} ${e ? "are" : "is"} ${c || d ? "missing" : ""}${XenoLibOutdated || ZeresPluginLibraryOutdated ? c || d ? " and/or outdated" : "outdated" : ""}.`, a + })(), h = BdApi.findModuleByDisplayName("Text"), i = BdApi.findModuleByDisplayName("ConfirmModal"), + j = () => BdApi.alert(f, BdApi.React.createElement("span", { style: { color: "white" } }, BdApi.React.createElement("div", {}, g), `Due to a slight mishap however, you'll have to download the libraries yourself. This is not intentional, something went wrong, errors are in console.`, + d || ZeresPluginLibraryOutdated ? BdApi.React.createElement("div", {}, BdApi.React.createElement("a", { href: "https://betterdiscord.net/ghdl?id=2252", target: "_blank" }, "Click here to download ZeresPluginLibrary")) : null, c || XenoLibOutdated ? BdApi.React.createElement("div", {}, + BdApi.React.createElement("a", { href: "https://betterdiscord.net/ghdl?id=3169", target: "_blank" }, "Click here to download XenoLib")) : null)); if (global.ohgodohfuck) return; if (!b || !i || !h) return console.error(`Missing components:${(b ? "" : " ModalStack") + (i ? "" : " ConfirmationModalComponent") + (h ? "" : "TextElement")}`), + j(); class k extends BdApi.React.PureComponent { + constructor(a) { + super(a), this.state = { hasError: !1 }, this.componentDidCatch = a => (console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), "function" == typeof this.props.onError && this.props.onError(a)), + this.render = () => this.state.hasError ? null : this.props.children + } + } let l = !!global.DiscordJSucks, m = !1; const n = b.openModal(c => { + if (m) return null; try { + return BdApi.React.createElement(k, { label: "missing dependency modal", onError: () => (b.closeModal(n), j()) }, BdApi.React.createElement(i, + Object.assign({ + header: f, children: BdApi.React.createElement(h, { size: h.Sizes.SIZE_16, children: [`${g} Please click Download Now to download ${e ? "them" : "it"}.`] }), red: !1, confirmText: "Download Now", cancelText: "Cancel", onCancel: c.onClose, onConfirm: () => { + if (l) return; l = !0; const c = require("request"), d = require("fs"), + e = require("path"), f = BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, g = () => { + global.XenoLib && !XenoLibOutdated || c("https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js", (c, g, h) => { + try { + if (c || 200 !== g.statusCode) return b.closeModal(n), + j(); d.writeFile(e.join(f, "1XenoLib.plugin.js"), h, () => { BdApi.isSettingEnabled("fork-ps-5") && !a || BdApi.Plugins.reload(this.getName()) }) + } catch (a) { console.error("Fatal error downloading XenoLib", a), b.closeModal(n), j() } + }) + }; !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated ? c("https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js", + (a, c, h) => { try { if (a || 200 !== c.statusCode) return b.closeModal(n), j(); d.writeFile(e.join(f, "0PluginLibrary.plugin.js"), h, () => { }), g() } catch (a) { console.error("Fatal error downloading ZeresPluginLibrary", a), b.closeModal(n), j() } }) : g() + } + }, c, { onClose: () => { } }))) + } catch (a) { return console.error("There has been an error constructing the modal", a), m = !0, b.closeModal(n), j(), null } + }, { modalKey: `${this.name}_DEP_MODAL` }); + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config), BasePlugin); + })(); + + /*@end@*/ \ No newline at end of file