├── .gitignore ├── README.md ├── manifest.json ├── style.scss ├── components ├── Reactors.jsx └── Settings.jsx ├── LICENSE └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## who-reacted 2 | A plugin for [Powercord](https://powercord.dev/) that shows the avatars of the users who reacted to a message. 3 | 4 | ![Demo](https://i.imgur.com/dvwN7qr_d.webp?maxwidth=760&fidelity=grand) -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Who Reacted", 3 | "version": "1.1.0", 4 | "description": "Shows the avatars of the users who reacted to a message.", 5 | "author": "Jaime Filho (Marmota)", 6 | "license": "MIT" 7 | } -------------------------------------------------------------------------------- /style.scss: -------------------------------------------------------------------------------- 1 | .powercord-who-reacted-reactors { 2 | margin-left: 6px; 3 | 4 | .more-reactors { 5 | background-color: var(--background-tertiary); 6 | color: var(--text-normal); 7 | font-weight: 500; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /components/Reactors.jsx: -------------------------------------------------------------------------------- 1 | const { React, Flux, getModule, getModuleByDisplayName } = require('powercord/webpack'); 2 | 3 | const ReactionStore = getModule([ 'getReactions', '_dispatcher' ], false); 4 | const VoiceUserSummaryItem = getModuleByDisplayName('VoiceUserSummaryItem', false); 5 | 6 | const Reactors = ({ count, max, users }) => { 7 | function renderMoreUsers (text, className) { 8 | return ( 9 |
10 | +{1 + count - max} 11 |
12 | ); 13 | } 14 | 15 | return ( 16 | 22 | ); 23 | } 24 | 25 | module.exports = Flux.connectStores([ ReactionStore ], ({ message, emoji }) => ({ 26 | users: Object.values(ReactionStore.getReactions(message.getChannelId(), message.id, emoji) ?? {}) 27 | }))(Reactors); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Jaime Filho 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /components/Settings.jsx: -------------------------------------------------------------------------------- 1 | const { React } = require('powercord/webpack'); 2 | const { TextInput, SliderInput, SwitchItem } = require('powercord/components/settings'); 3 | 4 | const Settings = ({ getSetting, updateSetting }) => { 5 | function getThresholdMarkerLabel (value) { 6 | if (value === 0) { 7 | return 'Off'; 8 | } 9 | 10 | if (value % 1000 === 0) { 11 | return `${value / 1000}k`; 12 | } 13 | 14 | return value; 15 | } 16 | 17 | return ( 18 |
19 | { 24 | if (isNaN(value) || value < 0 || value > 99) { 25 | return; 26 | } 27 | 28 | updateSetting('maxUsersShown', value); 29 | }} 30 | > 31 | Max users shown 32 | 33 | updateSetting('reactionThreshold', value)} 44 | > 45 | Reaction threshold 46 | 47 | updateSetting('userThreshold', value)} 59 | > 60 | User threshold 61 | 62 | updateSetting('useHighestUserCount', value)} 66 | > 67 | Use highest user count 68 | 69 |
70 | ); 71 | }; 72 | 73 | module.exports = Settings; 74 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Plugin } = require('powercord/entities'); 2 | const { React, getModule } = require('powercord/webpack'); 3 | const { inject, uninject } = require('powercord/injector'); 4 | const { findInTree, getReactInstance, waitFor } = require('powercord/util'); 5 | 6 | const Settings = require('./components/Settings'); 7 | const Reactors = require('./components/Reactors'); 8 | 9 | module.exports = class WhoReacted extends Plugin { 10 | constructor () { 11 | super(); 12 | this.selectors = { 13 | reaction: `.${getModule([ 'reactions', 'reaction' ], false).reaction}` 14 | }; 15 | } 16 | 17 | async startPlugin () { 18 | await this.loadStylesheet('style.scss'); 19 | 20 | powercord.api.settings.registerSettings('who-reacted', { 21 | category: this.entityID, 22 | label: 'Who Reacted', 23 | render: Settings 24 | }); 25 | 26 | await this._patchReaction(); 27 | } 28 | 29 | pluginWillUnload () { 30 | powercord.api.settings.unregisterSettings('who-reacted'); 31 | 32 | uninject('who-reacted-reactors'); 33 | this._forceUpdateAllReactions(); 34 | } 35 | 36 | async _patchReaction () { 37 | const Reaction = await this._findReaction(); 38 | 39 | const { settings } = this; 40 | 41 | function canShowReactors ({ reactions }) { 42 | const reactionThreshold = settings.get('reactionThreshold', 10); 43 | if (reactionThreshold !== 0 && reactions.length > reactionThreshold) { 44 | return false; 45 | } 46 | 47 | const userThreshold = settings.get('userThreshold', 100); 48 | if (userThreshold !== 0) { 49 | const userCount = settings.get('useHighestUserCount', true) ? 50 | Math.max(...reactions.map(reaction => reaction.count)) : 51 | reactions.reduce((total, reaction) => total + reaction.count, 0); 52 | 53 | if (userCount > userThreshold) { 54 | return false; 55 | } 56 | } 57 | 58 | return true; 59 | } 60 | 61 | inject('who-reacted-reactors', Reaction.prototype, 'render', function (args, result) { 62 | const { message, emoji, count } = this.props; 63 | 64 | if (canShowReactors(message)) { 65 | const renderTooltip = result.props.children; 66 | result.props.children = props => { 67 | const tooltip = renderTooltip(props); 68 | const popout = tooltip.props.children.props.children; 69 | 70 | const renderReactionInner = popout.props.children; 71 | popout.props.children = props => { 72 | const reactionInner = renderReactionInner(props); 73 | 74 | reactionInner.props.children.push(React.createElement(Reactors, { 75 | message, 76 | emoji, 77 | count, 78 | max: settings.get('maxUsersShown', 6) 79 | })); 80 | 81 | return reactionInner; 82 | }; 83 | 84 | return tooltip; 85 | }; 86 | } 87 | 88 | return result; 89 | }); 90 | 91 | this._forceUpdateAllReactions(); 92 | } 93 | 94 | async _findReaction () { 95 | return this._findReactionReactElement(await waitFor(this.selectors.reaction)).type; 96 | } 97 | 98 | // Thanks @Juby210 99 | _forceUpdateAllReactions () { 100 | for (const element of document.querySelectorAll(this.selectors.reaction)) { 101 | this._findReactionReactElement(element).stateNode.forceUpdate(); 102 | } 103 | } 104 | 105 | _findReactionReactElement (node) { 106 | return findInTree(getReactInstance(node), r => r?.type?.displayName === 'Reaction', { 107 | walkable: [ 'return' ] 108 | }); 109 | } 110 | }; 111 | --------------------------------------------------------------------------------