From 1a84eb79e01a6c7ec125e3a604567a83586e1e6c Mon Sep 17 00:00:00 2001 From: _Lighty_ Date: Wed, 19 Feb 2020 21:47:08 +0100 Subject: [PATCH] BTU v1.0.0 release --- .../BetterTypingUsers.plugin.js | 444 ++++++++++++++++++ Plugins/BetterTypingUsers/CHANGELOG.md | 4 + Plugins/BetterTypingUsers/README.md | 14 + 3 files changed, 462 insertions(+) create mode 100644 Plugins/BetterTypingUsers/BetterTypingUsers.plugin.js create mode 100644 Plugins/BetterTypingUsers/CHANGELOG.md create mode 100644 Plugins/BetterTypingUsers/README.md diff --git a/Plugins/BetterTypingUsers/BetterTypingUsers.plugin.js b/Plugins/BetterTypingUsers/BetterTypingUsers.plugin.js new file mode 100644 index 0000000..c59d0ff --- /dev/null +++ b/Plugins/BetterTypingUsers/BetterTypingUsers.plugin.js @@ -0,0 +1,444 @@ +//META{"name":"BetterTypingUsers","source":"https://github.com/1Lighty/BetterDiscordPlugins/blob/master/Plugins/BetterTypingUsers/BetterTypingUsers.plugin.js","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=BetterTypingUsers"}*// +/*@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. + */ +var BetterTypingUsers = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'BetterTypingUsers', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: 'LightyPon', + twitter_username: '' + } + ], + version: '1.0.0', + 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: 'Initial release!', + type: 'added', + items: ['Max users typing changed to 5, with showing and "x others" if more people than that are typing.'] + }, + { + title: 'future plans', + type: 'progress', + items: ['Auto fit max number of people typing, until there is no space left.'] + } + ], + 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 { + onStart() { + this.promises = { state: { cancelled: false } }; + this.patchAll(); + PluginUtilities.addStyle( + this.short + '-CSS', + ` + .BTU-preview > .${WebpackModules.getByProps('slowModeIcon', 'typing').typing.split(' ')[0]} { + position: unset !important; + } + ` + ); + } + + 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', (_, __, ret) => ret.slice(0, this.settings.maxVisible)); + } + + async patchTypingUsers(promiseState) { + const TypingUsers = await ReactComponents.getComponentByName('TypingUsers', 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.instead(TypingUsers.component.prototype, 'render', (_this, _, orig) => { + const ret = orig(); + const filtered = this.filterTypingUsers(_this.props.typingUsers); + if (filtered.length <= 3) return ret; + const typingUsers = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf(TypingTextClassname) !== -1); + if (!typingUsers) return ret; + 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 === 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 && typeof BdApi.getPlugin === '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); + const iZeresPluginLibrary = BdApi.getPlugin('ZeresPluginLibrary'); + if (isOutOfDate(iZeresPluginLibrary, '1.2.10')) ZeresPluginLibraryOutdated = true; + } + } catch (e) { + console.error('Error checking if ZeresPluginLibrary is out of date', e); + } + + return !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated + ? class { + getName() { + return this.name.replace(/\s+/g, ''); + } + getAuthor() { + return this.author; + } + getVersion() { + return this.version; + } + getDescription() { + return this.description; + } + stop() {} + load() { + const header = ZeresPluginLibraryOutdated ? 'Outdated Library' : 'Missing Library'; + const content = `The Library ZeresPluginLibrary required for ${this.name} is ${ZeresPluginLibraryOutdated ? 'outdated' : 'missing'}.`; + const ModalStack = BdApi.findModuleByProps('push', 'update', 'pop', 'popWithKey'); + const TextElement = BdApi.findModuleByProps('Sizes', 'Weights'); + const ConfirmationModal = BdApi.findModule(m => m.defaultProps && m.key && m.key() === 'confirm-modal'); + const onFail = () => BdApi.getCore().alert(header, `${content}
Due to a slight mishap however, you'll have to download the library yourself.

Click here to download ZeresPluginLibrary`); + if (!ModalStack || !ConfirmationModal || !TextElement) return onFail(); + class TempErrorBoundary extends BdApi.React.PureComponent { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + componentDidCatch(err, inf) { + console.error(`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; + } + } + let modalId; + const onHeckWouldYouLookAtThat = (() => { + if (!global.pluginModule || !global.BDEvents) return () => {}; /* other client mods */ + const onLibLoaded = e => { + if (e !== 'ZeresPluginLibrary') return; + BDEvents.off('plugin-loaded', onLibLoaded); + BDEvents.off('plugin-reloaded', onLibLoaded); + ModalStack.popWithKey(modalId); /* make it easier on the user */ + pluginModule.reloadPlugin(this.getName()); + }; + BDEvents.on('plugin-loaded', onLibLoaded); + BDEvents.on('plugin-reloaded', onLibLoaded); + return () => (BDEvents.off('plugin-loaded', onLibLoaded), BDEvents.off('plugin-reloaded', onLibLoaded)); + })(); + modalId = ModalStack.push(props => { + return BdApi.React.createElement( + TempErrorBoundary, + { + label: 'missing/outdated dependency modal', + onError: () => { + ModalStack.popWithKey(modalId); /* smh... */ + onFail(); + } + }, + BdApi.React.createElement( + ConfirmationModal, + Object.assign( + { + header, + children: [ + BdApi.React.createElement(TextElement, { + color: TextElement.Colors.PRIMARY, + children: [`${content} Please click Download Now to download it.`] + }) + ], + red: false, + confirmText: 'Download Now', + cancelText: 'Cancel', + onConfirm: () => { + onHeckWouldYouLookAtThat(); + const request = require('request'); + const fs = require('fs'); + const path = require('path'); + const onDone = () => { + if (!global.pluginModule || !global.BDEvents) return; + const onLoaded = e => { + if (e !== 'ZeresPluginLibrary') return; + BDEvents.off('plugin-loaded', onLoaded); + BDEvents.off('plugin-reloaded', onLoaded); + pluginModule.reloadPlugin(this.name); + }; + BDEvents.on('plugin-loaded', onLoaded); + BDEvents.on('plugin-reloaded', onLoaded); + }; + request('https://rauenzi.github.io/BDPluginLibrary/release/0PluginLibrary.plugin.js', (error, response, body) => { + if (error) return onFail(); + onDone(); + fs.writeFile(path.join(window.ContentManager.pluginsFolder, '0PluginLibrary.plugin.js'), body, () => {}); + }); + } + }, + props + ) + ) + ); + }); + } + + start() {} + 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@*/ diff --git a/Plugins/BetterTypingUsers/CHANGELOG.md b/Plugins/BetterTypingUsers/CHANGELOG.md new file mode 100644 index 0000000..8cc138f --- /dev/null +++ b/Plugins/BetterTypingUsers/CHANGELOG.md @@ -0,0 +1,4 @@ +# [BetterTypingUsers](https://1lighty.github.io/BetterDiscordStuff/?plugin=BetterTypingUsers "BetterTypingUsers") Changelog +### 1.0.0 +- Initial release +- Max users typing changed to 5, with showing and "x others" if more people than that are typing. diff --git a/Plugins/BetterTypingUsers/README.md b/Plugins/BetterTypingUsers/README.md new file mode 100644 index 0000000..5243a36 --- /dev/null +++ b/Plugins/BetterTypingUsers/README.md @@ -0,0 +1,14 @@ +# BetterTypingUsers [![download](https://i.imgur.com/OAHgjZu.png)](https://1lighty.github.io/BetterDiscordStuff/?plugin=BetterTypingUsers&dl=1 "BetterTypingUsers") +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. +### Features +Several people are typing... +is changed to +**user 1**, **user 2**, **user 3** and **user 4** are typing +or if it can't fit +**user 1**, **user 2**, **user 3** and 3 others are typing +It is 100% compatible with [BetterRoleColors](https://1lighty.github.io/BetterDiscordStuff/?plugin=BetterTypingUsers&dl=1 "BetterRoleColors") as well. +### Settings +##### Max visible typing users +Max number of visible users typing, can be set between 3 and 20. If more people are typing than this is set to, it will show "x others" instead. +### Preview +![typing](https://i.imgur.com/HcPkMOx.png) \ No newline at end of file