├── .prettierrc.js ├── Components ├── Settings.jsx └── StringPart.jsx ├── LICENSE ├── README.md ├── index.js ├── manifest.json └── tooltips.js /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | jsxSingleQuote: true, 4 | trailingComma: 'none', 5 | useTabs: false, 6 | tabWidth: 4, 7 | semi: true, 8 | arrowParens: 'avoid', 9 | jsxBracketSameLine: true 10 | }; 11 | -------------------------------------------------------------------------------- /Components/Settings.jsx: -------------------------------------------------------------------------------- 1 | const { React } = require('powercord/webpack'); 2 | const { SwitchItem, Category } = require('powercord/components/settings'); 3 | 4 | const { tooltips } = require('../tooltips'); 5 | 6 | // TODO: Rewrite this messy file 7 | 8 | module.exports = class Settings extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { opened: { main: true } }; 12 | } 13 | 14 | toSnake(str) { 15 | return str.split(' ').join('-').toLowerCase(); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 | 26 | this.setState({ 27 | ...this.state.opened, 28 | opened: { main: !this.state.opened.main } 29 | }) 30 | }> 31 | {tooltips.map(item => { 32 | const id = `tooltip-toggled-${this.toSnake(item.name)}`; 33 | return ( 34 | { 37 | this.props.toggleSetting(id, item.default); 38 | }} 39 | note={item.description}> 40 | {item.name} 41 | 42 | ); 43 | })} 44 | 45 | 46 | {tooltips.map(item => { 47 | if ( 48 | this.props.getSetting( 49 | `tooltip-toggled-${this.toSnake(item.name)}`, 50 | item.default 51 | ) && 52 | item.options?.length > 0 53 | ) { 54 | return ( 55 | 62 | this.setState({ 63 | ...this.state.opened, 64 | opened: { 65 | [this.toSnake(item.name)]: !this 66 | .state.opened[ 67 | this.toSnake(item.name) 68 | ] 69 | } 70 | }) 71 | }> 72 | {item.options?.map(option => ( 73 | { 79 | this.props.toggleSetting( 80 | option.id, 81 | option.default 82 | ); 83 | }} 84 | note={option.note}> 85 | {option.name} 86 | 87 | ))} 88 | 89 | ); 90 | } 91 | })} 92 |
93 | ); 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /Components/StringPart.jsx: -------------------------------------------------------------------------------- 1 | const { React, getModuleByDisplayName } = require('powercord/webpack'); 2 | 3 | const Tooltip = getModuleByDisplayName('Tooltip', false); 4 | 5 | class StringPart extends React.PureComponent { 6 | render() { 7 | const { parts, ops } = this.props; 8 | 9 | /** 10 | * Iterate through every item in {parts}, knowing that the items that need to 11 | * be replaced will be on every odd numbered index. 12 | */ 13 | for (var i = 1; i < parts.length; i += 2) { 14 | if (typeof parts[i] !== 'string') continue; 15 | const text = parts[i]; 16 | const display = this.selectTooltip(this.props.name, parts[i], ops); 17 | 18 | if (display) 19 | parts[i] = ( 20 | 21 | {props => {text}} 22 | 23 | ); 24 | } 25 | 26 | return parts; 27 | } 28 | 29 | selectTooltip(name, part, ops) { 30 | /** 31 | * Add tooltip content here. 32 | * Return either a string, react element, or NULL to cancel. 33 | */ 34 | switch (name) { 35 | case 'Color Codes': 36 | return ( 37 | 47 | ); 48 | case 'Base64': 49 | const parsed = Buffer.from(part, 'base64').toString('binary'); 50 | // Honestly the base64-majority-text option confused me and I only got it correct via trial and error. 51 | // prettier-ignore 52 | if (ops['base64-majority-text'] && parsed.replace(/[a-zA-Z0-9\t\n .\/<>?;:"'`!@#$%^&*()\[\]{}_+=|\\-]/g, '').length > parsed.length * .25) return null; 53 | else return parsed; 54 | default: 55 | return part; 56 | } 57 | } 58 | } 59 | 60 | module.exports = StringPart; 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Loren Cerri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # powercord-message-tooltips 2 | 3 | A simple plugin that displays useful information as tooltips in messages. 4 | 5 | ## Settings 6 | 7 | - _Individual tooltip toggles_ 8 | 9 | ## Tooltips 10 | 11 | _Have a suggestion? Either create an issue on this GitHub repo or send me a message on Discord!_ **_(TrueXPixels#2113)_** 12 | 13 | | Title | Preview | 14 | | :------------------: | :-------------------------------------: | 15 | | Color Codes | ![](https://i.imgur.com/BWgytsA.gif) | 16 | | Base64[1]
**[Experimental]** | ![](https://i.imgur.com/IH8QKeQ.gif) | 17 | 18 | **1:** There are issues if the Base64 string is part of a larger message, although it works perfectly fine when it's the entire messsage. 19 | 20 | 21 | 22 | ## Hey There 👋 23 | 24 | I work on these projects in my spare time, if you'd like to support me, you can do so via [Patreon! ❤️](https://www.patreon.com/lorencerri) 25 | 26 | ***Check out my other plugins:** [lorencerri.github.io/?tag=powercord](https://lorencerri.github.io/?tag=powercord)* 27 | 28 | **Twitter:** [twitter.com/lorencerri](https://twitter.com/lorencerri)
29 | **Discord:** [discord.gg/plexidev](https://discord.gg/plexidev) 30 | 31 | > Need a custom Discord bot or project completed? Feel free to send me a message on [Discord](https://discord.gg/plexidev) (lorencerri#2113) or [Twitter](https://twitter.com/lorencerri)! 32 | 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * powercord-message-tooltips 3 | * https://github.com/TrueXPixels/powercord-message-tooltips 4 | */ 5 | 6 | const { Plugin } = require('powercord/entities'); 7 | const { inject, uninject } = require('powercord/injector'); 8 | const { React, getModule } = require('powercord/webpack'); 9 | const { tooltips } = require('./tooltips.js'); 10 | 11 | const StringPart = require('./Components/StringPart'); 12 | const Settings = require('./Components/Settings'); 13 | 14 | module.exports = class MessageTooltips extends Plugin { 15 | async startPlugin() { 16 | powercord.api.settings.registerSettings(this.entityID, { 17 | category: this.entityID, 18 | label: 'Message Tooltips', 19 | render: Settings 20 | }); 21 | 22 | const parser = await getModule(['parse', 'parseTopic']); 23 | const process = this.process.bind(this); 24 | 25 | inject(`message-tooltips`, parser, 'parse', process); 26 | inject(`embed-tooltips`, parser, 'parseAllowLinks', process); 27 | inject(`topic-tooltips`, parser, 'parseTopic', (a, b) => 28 | process(a, b, { position: 'bottom' }) 29 | ); 30 | } 31 | 32 | /** 33 | * Processes a message component 34 | * @param {*} args - Arguments, rarely used 35 | * @param {*} res - The message componenet being passed through the function 36 | * @param {*} ops - Additional options 37 | */ 38 | process(args, res, ops = {}) { 39 | // Iterate through every tooltip 40 | for (var i = 0; i < tooltips.length; i++) { 41 | // Continue if the tooltip is not enabled 42 | const id = `tooltip-toggled-${this.toSnake(tooltips[i].name)}`; 43 | if (!this.settings.get(id, tooltips[i].default)) continue; 44 | 45 | /** 46 | * Replace the following property with a version that 47 | * may have replaced the nested strings with React elements 48 | */ 49 | if (res?.props?.children[1]) 50 | res.props.children[1] = this.replace( 51 | res.props.children[1], 52 | tooltips[i], 53 | ops 54 | ); 55 | else if (Array.isArray(res)) 56 | res = this.replace(res, tooltips[i], ops); 57 | } 58 | return res; 59 | } 60 | 61 | /** 62 | * Recursively replaces the string elements in the nested arrays with custom elements 63 | * @param {*} base - The .children property of the props 64 | * @param {*} item - The current regex item being parsed against a string 65 | * @param {*} ops - Additional options 66 | */ 67 | replace(base, item, ops) { 68 | // Return a remapped version of the base 69 | return base.map(i => { 70 | if (typeof i === 'string' && i.trim()) { 71 | /** 72 | * If {i} is a valid, non-whitespace string, parse it against the regex item 73 | * to see if it needs to be replaced with a tooltip element 74 | */ 75 | 76 | return this.getElement(i, item, ops); 77 | } else if (Array.isArray(i?.props?.children)) 78 | // Otherwise, if {i} has valid .props.children, reiterate through that instead 79 | 80 | return { 81 | ...i, 82 | props: { 83 | ...i.props, 84 | children: this.replace(i.props.children, item, ops) 85 | } 86 | }; 87 | else { 88 | /** 89 | * If none of the previous clauses are true, it's most likely just a design element 90 | * such as a block quote or image, which can simply be returned. 91 | */ 92 | 93 | // Handle Inline Code 94 | if ( 95 | i.type === 'code' && 96 | typeof i?.props?.children === 'string' && 97 | i?.props?.children?.trim() 98 | ) 99 | i.props.children = this.getElement( 100 | i.props.children, 101 | item, 102 | ops 103 | ); 104 | 105 | return i; 106 | } 107 | }); 108 | } 109 | 110 | getElement(i, item, ops) { 111 | const parts = i.split(item.regex); 112 | 113 | /** 114 | * If the regex does not match the string, {parts} will contain an array of 115 | * either one or zero length. Therefore, we can just return the string as we 116 | * don't need to do anything to it. 117 | */ 118 | if (parts.length <= 1) return i; 119 | 120 | // Parse & Pass Options 121 | for (var x = 0; x < item.options?.length; x++) 122 | ops[item.options[x].id] = this.settings.get( 123 | item.options[x].id, 124 | item.options[x].default 125 | ); 126 | 127 | /** 128 | * If the regex matched the string, {parts} will now contain an array of elements 129 | * that need to be replaced with tooltips at every odd number index. Return the 130 | * replacement tooltip element instead of the string. 131 | */ 132 | 133 | return React.createElement(StringPart, { 134 | parts, 135 | ops, 136 | regex: item.regex, 137 | name: item.name 138 | }); 139 | } 140 | 141 | pluginWillUnload() { 142 | powercord.api.settings.unregisterSettings(this.entityID); 143 | uninject('message-tooltips'); 144 | uninject('embed-tooltips'); 145 | uninject('topic-tooltips'); 146 | } 147 | 148 | /** 149 | * Helper Functions 150 | */ 151 | toSnake(str) { 152 | return str.split(' ').join('-').toLowerCase(); 153 | } 154 | }; 155 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Message Tooltips", 3 | "version": "1.0.0", 4 | "description": "A simple plugin that displays useful information as tooltips in messages.", 5 | "author": "TrueXPixels", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /tooltips.js: -------------------------------------------------------------------------------- 1 | /** 2 | * How to add a tooltip: 3 | * 1. Add the detection to the array below 4 | * 2. Add what should appear in the tooltip in ./Components/StringPart.jsx 5 | */ 6 | 7 | exports.tooltips = [ 8 | { 9 | name: 'Color Codes', 10 | description: 'Displays a previews of color codes', 11 | regex: new RegExp( 12 | /((?:#|0x)(?:[a-f0-9]{3}|[a-f0-9]{6})\b|(?:rgb|hsl)a?\([^\)]*\))/, 13 | 'gmi' 14 | ), 15 | default: true 16 | }, 17 | { 18 | name: 'Base64', 19 | description: 'Displays Base64 strings decoded into normal text', 20 | regex: new RegExp( 21 | /(^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$)/, 22 | 'gm' 23 | ), 24 | default: true, 25 | options: [ 26 | { 27 | name: 'Require Majority Text', 28 | note: 29 | 'Require more than 75% of the text to be a valid US keyboard character. Reduces false positives.', 30 | id: 'base64-majority-text', 31 | default: true 32 | } 33 | ] 34 | } 35 | ]; 36 | --------------------------------------------------------------------------------