├── .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 |  |
16 | | Base64[1]
**[Experimental]** |  |
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 |
--------------------------------------------------------------------------------