├── .gitattributes ├── packages ├── WhoReacted │ ├── src │ │ ├── environment.js │ │ ├── hooks │ │ │ └── useStateFromStores.js │ │ ├── styles.scss │ │ ├── components │ │ │ ├── Reactor.jsx │ │ │ ├── Reactors.jsx │ │ │ └── SettingsPanel.jsx │ │ ├── stores │ │ │ └── SettingsStore.js │ │ └── index.jsx │ ├── webpack.config.js │ ├── package.json │ └── README.md ├── SecretRingTone │ ├── webpack.config.js │ ├── package.json │ ├── src │ │ └── index.js │ └── README.md └── BiggerStreamPreview │ ├── webpack.config.js │ ├── package.json │ ├── README.md │ └── src │ └── index.jsx ├── .gitignore ├── lerna.json ├── .editorconfig ├── package.json ├── .github └── workflows │ └── build.yml ├── LICENSE ├── README.md └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /packages/WhoReacted/src/environment.js: -------------------------------------------------------------------------------- 1 | export const BoundedBdApi = new BdApi('WhoReacted'); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | 4 | node_modules 5 | dist 6 | 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "useWorkspaces": true, 4 | "version": "0.0.0" 5 | } 6 | -------------------------------------------------------------------------------- /packages/WhoReacted/webpack.config.js: -------------------------------------------------------------------------------- 1 | const generatePluginConfig = require("../../webpack.config"); 2 | 3 | module.exports = generatePluginConfig(__dirname); 4 | -------------------------------------------------------------------------------- /packages/SecretRingTone/webpack.config.js: -------------------------------------------------------------------------------- 1 | const generatePluginConfig = require("../../webpack.config"); 2 | 3 | module.exports = generatePluginConfig(__dirname); 4 | -------------------------------------------------------------------------------- /packages/BiggerStreamPreview/webpack.config.js: -------------------------------------------------------------------------------- 1 | const generatePluginConfig = require("../../webpack.config"); 2 | 3 | module.exports = generatePluginConfig(__dirname); 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /packages/WhoReacted/src/hooks/useStateFromStores.js: -------------------------------------------------------------------------------- 1 | import { BoundedBdApi } from '../environment'; 2 | 3 | const { Webpack } = BoundedBdApi; 4 | const { Filters } = Webpack; 5 | 6 | export const useStateFromStores = Webpack.getModule( 7 | Filters.byStrings('useStateFromStores'), 8 | { searchExports: true } 9 | ); 10 | -------------------------------------------------------------------------------- /packages/WhoReacted/src/styles.scss: -------------------------------------------------------------------------------- 1 | .bd-who-reacted__reactors { 2 | display: flex; 3 | align-items: center; 4 | 5 | &:not(:empty) { 6 | margin-left: 8px; 7 | } 8 | } 9 | 10 | .bd-who-reacted__reactor-avatar { 11 | border-radius: 50%; 12 | } 13 | 14 | .bd-who-reacted__more-reactors { 15 | box-sizing: border-box; 16 | 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | 21 | color: var(--text-normal); 22 | font-weight: 500; 23 | 24 | background-color: var(--background-tertiary); 25 | } 26 | -------------------------------------------------------------------------------- /packages/WhoReacted/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WhoReacted", 3 | "author": "Marmota (Jaime Filho)", 4 | "authorLink": "https://github.com/jaimeadf", 5 | "version": "1.3.1", 6 | "description": "Shows the avatars of the users who reacted to a message.", 7 | "source": "https://github.com/jaimeadf/BetterDiscordPlugins/tree/main/packages/WhoReacted", 8 | "main": "src/index.jsx", 9 | "private": true, 10 | "scripts": { 11 | "build": "webpack --mode production", 12 | "build:dev": "webpack --mode development", 13 | "watch:dev": "webpack --watch --mode development" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/BiggerStreamPreview/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BiggerStreamPreview", 3 | "author": "Marmota (Jaime Filho)", 4 | "authorLink": "https://github.com/jaimeadf", 5 | "version": "1.1.3", 6 | "description": "View bigger stream previews via the context menu.", 7 | "source": "https://github.com/jaimeadf/BetterDiscordPlugins/tree/main/packages/BiggerStreamPreview", 8 | "main": "src/index.jsx", 9 | "private": true, 10 | "scripts": { 11 | "build": "webpack --mode production", 12 | "build:dev": "webpack --mode development", 13 | "watch:dev": "webpack --watch --mode development" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/SecretRingTone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SecretRingTone", 3 | "author": "Marmota (Jaime Filho)", 4 | "authorLink": "https://github.com/jaimeadf", 5 | "version": "1.1.0", 6 | "description": "Forces discord to play the secret ringtone every time someone calls you instead of the 1 in a 1000 chance of it happening.", 7 | "source": "https://github.com/jaimeadf/BetterDiscordPlugins/tree/main/packages/SecretRingTone", 8 | "main": "src/index.js", 9 | "private": true, 10 | "scripts": { 11 | "build": "webpack --mode production", 12 | "build:dev": "webpack --mode development", 13 | "watch:dev": "webpack --watch --mode development" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bd-plugins", 3 | "author": "Jaime Filho ", 4 | "repository": "https://github.com/jaimeadf/BetterDiscordPlugins", 5 | "license": "MIT", 6 | "private": true, 7 | "workspaces": [ 8 | "packages/*" 9 | ], 10 | "scripts": { 11 | "build": "lerna run build", 12 | "build:dev": "lerna run build:dev", 13 | "watch:dev": "lerna run watch:dev" 14 | }, 15 | "devDependencies": { 16 | "@sucrase/webpack-loader": "^2.0.0", 17 | "copy-webpack-plugin": "^11.0.0", 18 | "lerna": "^6.1.0", 19 | "raw-loader": "^4.0.2", 20 | "react": "^18.2.0", 21 | "sass": "^1.60.0", 22 | "sass-loader": "^13.2.2", 23 | "sucrase": "^3.29.0", 24 | "webpack": "^5.75.0", 25 | "webpack-cli": "^5.0.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/SecretRingTone/src/index.js: -------------------------------------------------------------------------------- 1 | const BoundedBdApi = new BdApi('SecretRingTone'); 2 | 3 | const { Webpack, Patcher } = BoundedBdApi; 4 | const { Filters } = Webpack; 5 | 6 | const WebAudioSound = Webpack.getModule(Filters.byPrototypeFields('_ensureAudio'), { searchExports: true }); 7 | 8 | export default class SecretRingTone { 9 | constructor() { 10 | this.sounds = []; 11 | } 12 | 13 | start() { 14 | Patcher.before(WebAudioSound.prototype, '_ensureAudio', sound => { 15 | if (sound.name == 'call_ringing') { 16 | sound.name = 'call_ringing_beat'; 17 | this.sounds.push(sound); 18 | } 19 | }); 20 | } 21 | 22 | stop() { 23 | Patcher.unpatchAll(); 24 | 25 | for (const sound of this.sounds) { 26 | sound.name = 'call_ringing'; 27 | } 28 | 29 | this.sounds = []; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Set up node 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | - name: Install dependencies 17 | run: yarn install 18 | - name: Build 19 | run: yarn build 20 | - name: Aggregate build files 21 | run: rsync -rm --include="*/" --include="dist/*" --exclude="*" packages/* build 22 | - name: Push 23 | uses: s0/git-publish-subdir-action@develop 24 | env: 25 | REPO: self 26 | BRANCH: build 27 | FOLDER: build 28 | MESSAGE: "chore: build ${{ github.event.after }}" 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | SKIP_EMPTY_COMMITS: true 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 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. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BetterDiscordPlugins 2 | A collection of plugins for the [BetterDiscord](https://betterdiscord.app) client modification written by me with a lot of ☕. 3 | 4 | ## [BiggerStreamPreview](packages/BiggerStreamPreview) 5 | View bigger stream previews via the context menu. 6 | 7 | # Development 8 | This project uses [lerna](https://lerna.js.org) and [yarn workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces) to divide each plugin into its own isolated container inside the `packages` folder. 9 | 10 | ## Getting started 11 | ### Prerequisites 12 | - [Git](https://git-scm.com) 13 | - [Node.js](https://nodejs.org) 14 | - [Yarn](https://yarnpkg.com) 15 | 16 | ### Setup 17 | ```sh 18 | # 1. Clone the repository and navigate to its directory: 19 | git clone https://github.com/jaimeadf/BetterDiscordPlugins && cd BetterDiscordPlugins 20 | 21 | # 2. Install the dependencies: 22 | yarn install 23 | 24 | # 3. Run the `watch:dev` script: 25 | yarn watch:dev 26 | 27 | # Now, you may edit any plugin and see the changes take effect in real time. 28 | # Happy coding 🎉! 29 | ``` 30 | 31 | ## Scripts 32 | > Note: These scripts may be run for all plugins simultaneously from the project's root or independently from the plugin's directory. 33 | 34 | - `yarn build`: Builds the plugins for distribution. 35 | - `yarn build:dev`: Builds the plugins directly into your plugins folder. 36 | - `yarn watch:dev`: Watches for any changes and automatically builds the plugins directly into your plugins folder. 37 | -------------------------------------------------------------------------------- /packages/WhoReacted/src/components/Reactor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function Reactor({ user, guildId, size }) { 4 | return ( 5 | 11 | ); 12 | } 13 | 14 | export function MaskedReactor({ user, guildId, size, overlap, spacing }) { 15 | const proportionalInnerRadius = 1 / 2; 16 | const proportionalOuterRadius = proportionalInnerRadius + spacing; 17 | 18 | const absoluteOffset = (overlap - spacing) * size; 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/WhoReacted/src/stores/SettingsStore.js: -------------------------------------------------------------------------------- 1 | import { BoundedBdApi } from '../environment'; 2 | import { useStateFromStores } from '../hooks/useStateFromStores'; 3 | 4 | const { Webpack } = BoundedBdApi; 5 | const { Filters } = Webpack; 6 | 7 | const Flux = Webpack.getModule(Filters.byProps('Store', 'connectStores')); 8 | const Dispatcher = Webpack.getModule(Filters.byPrototypeFields('dispatch'), { searchExports: true }); 9 | 10 | export const ActionTypes = { 11 | SETTINGS_UPDATE: 'BD_WHO_REACTED_SETTINGS_UPDATE' 12 | }; 13 | 14 | class DefaultSettingsStore extends Flux.Store { 15 | constructor(defaults) { 16 | super(new Dispatcher(), { 17 | [ActionTypes.SETTINGS_UPDATE]: () => this.save() 18 | }); 19 | 20 | this.defaults = defaults; 21 | this.settings = {}; 22 | } 23 | 24 | update(name, value) { 25 | this.settings[name] = value; 26 | this._dispatchUpdate(); 27 | } 28 | 29 | getSettings() { 30 | return this.settings; 31 | } 32 | 33 | getDefaultSettings() { 34 | return this.defaults; 35 | } 36 | 37 | load() { 38 | this.settings = { ...this.defaults, ...BoundedBdApi.Data.load('settings') }; 39 | this._dispatchUpdate(); 40 | } 41 | 42 | save() { 43 | BoundedBdApi.Data.save('settings', this.settings); 44 | } 45 | 46 | _dispatchUpdate() { 47 | this._dispatcher.dispatch({ 48 | type: ActionTypes.SETTINGS_UPDATE 49 | }); 50 | } 51 | } 52 | 53 | export const SettingsStore = new DefaultSettingsStore({ 54 | max: 6, 55 | avatarSize: 24, 56 | avatarOverlap: 100 / 3, 57 | avatarSpacing: 100 / 12, 58 | emojiThreshold: 10, 59 | reactionsTotalThreshold: 500, 60 | reactionsPerEmojiThreshold: 100, 61 | hideSelf: false, 62 | hideBots: false, 63 | hideBlocked: false 64 | }); 65 | 66 | export function useSettings() { 67 | return useStateFromStores([SettingsStore], () => [ 68 | SettingsStore.getSettings(), 69 | SettingsStore.getDefaultSettings(), 70 | (name, value) => SettingsStore.update(name, value) 71 | ]); 72 | } 73 | -------------------------------------------------------------------------------- /packages/WhoReacted/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { BoundedBdApi } from './environment'; 4 | 5 | import { SmartReactors } from './components/Reactors' 6 | import { SettingsPanel } from './components/SettingsPanel'; 7 | 8 | import styles from './styles.scss'; 9 | import { SettingsStore } from './stores/SettingsStore'; 10 | 11 | const { Webpack, Patcher } = BoundedBdApi; 12 | 13 | const ConnectedReaction = Webpack.getModule(m => m?.type?.toString()?.includes('burstReactionsEnabled'), { searchExports: true }); 14 | 15 | export default class WhoReacted { 16 | constructor() { 17 | SettingsStore.initializeIfNeeded(); 18 | SettingsStore.load(); 19 | } 20 | 21 | start() { 22 | BoundedBdApi.DOM.addStyle(styles); 23 | this.patchReaction(); 24 | } 25 | 26 | stop() { 27 | BoundedBdApi.DOM.removeStyle(); 28 | Patcher.unpatchAll(); 29 | } 30 | 31 | getSettingsPanel() { 32 | return ; 33 | } 34 | 35 | patchReaction() { 36 | const unpatchConnectedReaction = Patcher.after(ConnectedReaction, 'type', (_, __, reaction) => { 37 | unpatchConnectedReaction(); 38 | 39 | Patcher.after(reaction.type.prototype, 'render', (thisObject, _, result) => { 40 | const { message, emoji, count, type } = thisObject.props; 41 | const renderTooltip = result.props.children[0].props.children; 42 | 43 | result.props.children[0].props.children = tooltipProps => { 44 | const tooltipChildren = renderTooltip(tooltipProps); 45 | const renderPopout = tooltipChildren.props.children.props.children.props.children; 46 | 47 | tooltipChildren.props.children.props.children.props.children = popoutProps => { 48 | const popoutChildren = renderPopout(popoutProps); 49 | 50 | popoutChildren.props.children.push( 51 | 57 | ); 58 | 59 | return popoutChildren; 60 | }; 61 | 62 | return tooltipChildren; 63 | }; 64 | }); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/WhoReacted/README.md: -------------------------------------------------------------------------------- 1 | # WhoReacted 2 | [![betterdiscord-badge]][betterdiscord-link] [![download-badge]][download-link] 3 | 4 | ## Overview 5 | 6 | Shows the avatars of the users who reacted to a message. 7 | 8 | ![Preview](https://user-images.githubusercontent.com/40345645/229246491-bb572a18-7e09-4457-ac20-b51e0fb05923.png) 9 | 10 | ## How to install 11 | 12 | 1. Download the plugin by clicking [here][download-link]. 13 | 2. Open your plugins folder by going to your settings, then to plugins and click the button at the top of the page. 14 | 3. Drag the file that you have just downloaded to there and make sure that it doesn't end with `(1)` or anything similar. 15 | 16 | [betterdiscord-link]: https://betterdiscord.app/plugin/WhoReacted 17 | [betterdiscord-badge]: https://img.shields.io/badge/betterdiscord.app-0c0d10?style=flat-square&logo=data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAACIuAAAiLgAAAAAAAAAAAAAQDQxwEA0M6hANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDOoQDQxwEA0M6RANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M6RANDP8QDQz/EA0M/w4MC/8MCwv/DAsL/wwLC/8MCwv/DgwM/w4LCv8NCgn/DwwL/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP9CKBj/Xjge/103Hv9dNx7/UzIc/zIhFv9EQUH/SUdG/xoYF/8OCwr/EA0M/xANDP8QDQz/EA0M/xANDP8RDgz/nFot/+qFP//mgz7/54M+/+SCPv/Lczf/lXBW/9nZ2f+wr6//KCYl/w8MCv8QDQz/EA0M/xANDP8QDQz/EQ0M/08wG/9yQyP/cEIi/3BBIv+gXC7/54M+/75tNf+3r6n//////4KAgP8NCgn/EA0M/xANDP8QDQz/EA0M/xANDP8NDAv/DQwL/xsTDv8TDw3/VjMc/+GAPf/IcTX/qp6W//////+pqKj/EQ4N/xANDP8QDQz/EA0M/xANDP8QDQz/EQ4M/184H/+GTij/pF4v/9F3Of/ohD//n1or/5iUkf//////rKuq/xEODf8QDQz/EA0M/xANDP8QDQz/DwwM/2E6H//bfTz/m1kt/6JdLv/Qdzn/6IQ//59aLP+YlJH//////6yrqv8RDg3/EA0M/xANDP8QDQz/EA0M/xEODP+cWi3/7YZA/4NMJ/8QDQz/VTMc/+GAPf/IcTX/qp6W//////+pp6f/EQ4N/xANDP8QDQz/EA0M/xANDP8RDgz/m1kt/+uFP/+wZTH/b0Ei/6BcLv/ngz7/vm01/7evqf//////f359/w0KCf8QDQz/EA0M/xANDP8QDQz/EQ4M/5pZLP/ohD//5YI+/+eDPv/kgj7/y3M3/5ZwV//Z2dn/rq2s/yckI/8PDAv/EA0M/xANDP8QDQz/EA0M/xANDP9CKBj/XTce/1w3Hv9cNx7/UzEb/zIhFv9FQkH/SEZG/xoXFv8OCwr/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/DgwL/wwLC/8MCwv/DAsL/wwLC/8ODAz/DgsK/w0KCf8PDAv/EA0M/xANDP8QDQz/EA0M/xANDOkQDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDOkQDQxwEA0M6hANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDOoQDQxwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== 18 | 19 | [download-link]: https://git-link.vercel.app/api/download?url=https://github.com/jaimeadf/BetterDiscordPlugins/blob/build/WhoReacted/dist/WhoReacted.plugin.js 20 | [download-badge]: https://img.shields.io/badge/dynamic/json?style=flat-square&color=3a71c1&labelColor=0c0d10&label=download&prefix=v&query=version&url=https://raw.githubusercontent.com/jaimeadf/BetterDiscordPlugins/main/packages/WhoReacted/package.json&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iZmVhdGhlciBmZWF0aGVyLWRvd25sb2FkIj48cGF0aCBkPSJNMjEgMTV2NGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMnYtNCI+PC9wYXRoPjxwb2x5bGluZSBwb2ludHM9IjcgMTAgMTIgMTUgMTcgMTAiPjwvcG9seWxpbmU+PGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjMiPjwvbGluZT48L3N2Zz4= 21 | -------------------------------------------------------------------------------- /packages/BiggerStreamPreview/README.md: -------------------------------------------------------------------------------- 1 | # BiggerStreamPreview 2 | [![betterdiscord-badge]][betterdiscord-link] [![download-badge]][download-link] 3 | 4 | ## Overview 5 | 6 | This plugin adds a button in the context menu to view bigger preview of streams. 7 | 8 | ![Preview](https://i.imgur.com/nWoOCCA.gif) 9 | 10 | ## How to install 11 | 12 | 1. Download the plugin by clicking [here][download-link]. 13 | 2. Open your plugins folder by going to your settings, then to plugins and click the button at the top of the page. 14 | 3. Drag the file that you have just downloaded to there and make sure that it doesn't end with `(1)` or anything similar. 15 | 16 | [betterdiscord-link]: https://betterdiscord.app/plugin/BiggerStreamPreview 17 | [betterdiscord-badge]: https://img.shields.io/badge/betterdiscord.app-0c0d10?style=flat-square&logo=data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAACIuAAAiLgAAAAAAAAAAAAAQDQxwEA0M6hANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDOoQDQxwEA0M6RANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M6RANDP8QDQz/EA0M/w4MC/8MCwv/DAsL/wwLC/8MCwv/DgwM/w4LCv8NCgn/DwwL/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP9CKBj/Xjge/103Hv9dNx7/UzIc/zIhFv9EQUH/SUdG/xoYF/8OCwr/EA0M/xANDP8QDQz/EA0M/xANDP8RDgz/nFot/+qFP//mgz7/54M+/+SCPv/Lczf/lXBW/9nZ2f+wr6//KCYl/w8MCv8QDQz/EA0M/xANDP8QDQz/EQ0M/08wG/9yQyP/cEIi/3BBIv+gXC7/54M+/75tNf+3r6n//////4KAgP8NCgn/EA0M/xANDP8QDQz/EA0M/xANDP8NDAv/DQwL/xsTDv8TDw3/VjMc/+GAPf/IcTX/qp6W//////+pqKj/EQ4N/xANDP8QDQz/EA0M/xANDP8QDQz/EQ4M/184H/+GTij/pF4v/9F3Of/ohD//n1or/5iUkf//////rKuq/xEODf8QDQz/EA0M/xANDP8QDQz/DwwM/2E6H//bfTz/m1kt/6JdLv/Qdzn/6IQ//59aLP+YlJH//////6yrqv8RDg3/EA0M/xANDP8QDQz/EA0M/xEODP+cWi3/7YZA/4NMJ/8QDQz/VTMc/+GAPf/IcTX/qp6W//////+pp6f/EQ4N/xANDP8QDQz/EA0M/xANDP8RDgz/m1kt/+uFP/+wZTH/b0Ei/6BcLv/ngz7/vm01/7evqf//////f359/w0KCf8QDQz/EA0M/xANDP8QDQz/EQ4M/5pZLP/ohD//5YI+/+eDPv/kgj7/y3M3/5ZwV//Z2dn/rq2s/yckI/8PDAv/EA0M/xANDP8QDQz/EA0M/xANDP9CKBj/XTce/1w3Hv9cNx7/UzEb/zIhFv9FQkH/SEZG/xoXFv8OCwr/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/DgwL/wwLC/8MCwv/DAsL/wwLC/8ODAz/DgsK/w0KCf8PDAv/EA0M/xANDP8QDQz/EA0M/xANDOkQDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDOkQDQxwEA0M6hANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDOoQDQxwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== 18 | 19 | [download-link]: https://git-link.vercel.app/api/download?url=https://github.com/jaimeadf/BetterDiscordPlugins/blob/build/BiggerStreamPreview/dist/BiggerStreamPreview.plugin.js 20 | [download-badge]: https://img.shields.io/badge/dynamic/json?style=flat-square&color=3a71c1&labelColor=0c0d10&label=download&prefix=v&query=version&url=https://raw.githubusercontent.com/jaimeadf/BetterDiscordPlugins/main/packages/BiggerStreamPreview/package.json&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iZmVhdGhlciBmZWF0aGVyLWRvd25sb2FkIj48cGF0aCBkPSJNMjEgMTV2NGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMnYtNCI+PC9wYXRoPjxwb2x5bGluZSBwb2ludHM9IjcgMTAgMTIgMTUgMTcgMTAiPjwvcG9seWxpbmU+PGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjMiPjwvbGluZT48L3N2Zz4= 21 | -------------------------------------------------------------------------------- /packages/SecretRingTone/README.md: -------------------------------------------------------------------------------- 1 | # SecretRingTone 2 | [![betterdiscord-badge]][betterdiscord-link] [![download-badge]][download-link] 3 | 4 | ## Overview 5 | 6 | Forces discord to play the secret ringtone every time someone calls you instead of the 1 in a 1000 chance of it 7 | happening. Just so you know, you must be online, away, or invisible and not be DND (the red status) to hear it. 8 | 9 | ## How to install 10 | 11 | 1. Download the plugin by clicking [here][download-link]. 12 | 2. Open your plugins folder by going to your settings, then to plugins and click the button at the top of the page. 13 | 3. Drag the file that you have just downloaded to there and make sure that it doesn't end with `(1)` or anything similar. 14 | 15 | [betterdiscord-link]: https://betterdiscord.app/plugin/SecretRingTone 16 | [betterdiscord-badge]: https://img.shields.io/badge/betterdiscord.app-0c0d10?style=flat-square&logo=data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAACIuAAAiLgAAAAAAAAAAAAAQDQxwEA0M6hANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDOoQDQxwEA0M6RANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M6RANDP8QDQz/EA0M/w4MC/8MCwv/DAsL/wwLC/8MCwv/DgwM/w4LCv8NCgn/DwwL/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP9CKBj/Xjge/103Hv9dNx7/UzIc/zIhFv9EQUH/SUdG/xoYF/8OCwr/EA0M/xANDP8QDQz/EA0M/xANDP8RDgz/nFot/+qFP//mgz7/54M+/+SCPv/Lczf/lXBW/9nZ2f+wr6//KCYl/w8MCv8QDQz/EA0M/xANDP8QDQz/EQ0M/08wG/9yQyP/cEIi/3BBIv+gXC7/54M+/75tNf+3r6n//////4KAgP8NCgn/EA0M/xANDP8QDQz/EA0M/xANDP8NDAv/DQwL/xsTDv8TDw3/VjMc/+GAPf/IcTX/qp6W//////+pqKj/EQ4N/xANDP8QDQz/EA0M/xANDP8QDQz/EQ4M/184H/+GTij/pF4v/9F3Of/ohD//n1or/5iUkf//////rKuq/xEODf8QDQz/EA0M/xANDP8QDQz/DwwM/2E6H//bfTz/m1kt/6JdLv/Qdzn/6IQ//59aLP+YlJH//////6yrqv8RDg3/EA0M/xANDP8QDQz/EA0M/xEODP+cWi3/7YZA/4NMJ/8QDQz/VTMc/+GAPf/IcTX/qp6W//////+pp6f/EQ4N/xANDP8QDQz/EA0M/xANDP8RDgz/m1kt/+uFP/+wZTH/b0Ei/6BcLv/ngz7/vm01/7evqf//////f359/w0KCf8QDQz/EA0M/xANDP8QDQz/EQ4M/5pZLP/ohD//5YI+/+eDPv/kgj7/y3M3/5ZwV//Z2dn/rq2s/yckI/8PDAv/EA0M/xANDP8QDQz/EA0M/xANDP9CKBj/XTce/1w3Hv9cNx7/UzEb/zIhFv9FQkH/SEZG/xoXFv8OCwr/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/DgwL/wwLC/8MCwv/DAsL/wwLC/8ODAz/DgsK/w0KCf8PDAv/EA0M/xANDP8QDQz/EA0M/xANDOkQDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDOkQDQxwEA0M6hANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDP8QDQz/EA0M/xANDOoQDQxwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== 17 | 18 | [download-link]: https://git-link.vercel.app/api/download?url=https://github.com/jaimeadf/BetterDiscordPlugins/blob/build/SecretRingTone/dist/SecretRingTone.plugin.js 19 | [download-badge]: https://img.shields.io/badge/dynamic/json?style=flat-square&color=3a71c1&labelColor=0c0d10&label=download&prefix=v&query=version&url=https://raw.githubusercontent.com/jaimeadf/BetterDiscordPlugins/main/packages/SecretRingTone/package.json&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iZmVhdGhlciBmZWF0aGVyLWRvd25sb2FkIj48cGF0aCBkPSJNMjEgMTV2NGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMnYtNCI+PC9wYXRoPjxwb2x5bGluZSBwb2ludHM9IjcgMTAgMTIgMTUgMTcgMTAiPjwvcG9seWxpbmU+PGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjMiPjwvbGluZT48L3N2Zz4= 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const webpack = require("webpack"); 4 | const CopyPlugin = require("copy-webpack-plugin"); 5 | 6 | module.exports = function generatePluginConfig(pluginPath) { 7 | const manifest = readManifest(pluginPath); 8 | 9 | const bdHeader = buildBdHeader(manifest); 10 | const bdPluginsPath = getBdPluginsPath(); 11 | 12 | return (env, argv) => { 13 | const isProduction = argv.mode === "production"; 14 | 15 | return { 16 | target: "node", 17 | entry: pluginPath, 18 | output: { 19 | clean: isProduction, 20 | filename: `${manifest.name}.plugin.js`, 21 | path: isProduction ? path.join(pluginPath, "dist") : bdPluginsPath, 22 | library: { 23 | type: "commonjs2", 24 | export: "default" 25 | } 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.jsx?$/, 31 | exclude: /node_modules/, 32 | use: { 33 | loader: "@sucrase/webpack-loader", 34 | options: { 35 | production: isProduction, 36 | disableESTransforms: true, 37 | transforms: ["jsx"] 38 | } 39 | } 40 | }, 41 | { 42 | test: /\.css$/, 43 | use: 'raw-loader' 44 | }, 45 | { 46 | test: /\.scss$/, 47 | use: ['raw-loader', 'sass-loader'] 48 | } 49 | ] 50 | }, 51 | resolve: { 52 | extensions: ['.js', '.jsx'] 53 | }, 54 | optimization: { 55 | minimize: false 56 | }, 57 | plugins: [ 58 | new webpack.BannerPlugin({ banner: bdHeader.toString(), raw: true }), 59 | isProduction && new CopyPlugin({ 60 | patterns: [ 61 | path.join(pluginPath, "README.md") 62 | ] 63 | }) 64 | ].filter(Boolean), 65 | externals: { 66 | react: ["global BdApi", "React"] 67 | } 68 | } 69 | }; 70 | }; 71 | 72 | function readManifest(pluginPath) { 73 | return require(path.join(pluginPath, "package.json")); 74 | } 75 | 76 | function buildBdHeader(manifest) { 77 | const header = new BdHeader(); 78 | 79 | header.set("name", manifest.name); 80 | header.set("author", manifest.author); 81 | header.set("authorLink", manifest.authorLink); 82 | header.set("description", manifest.description); 83 | header.set("version", manifest.version); 84 | header.set("source", manifest.source); 85 | 86 | return header; 87 | } 88 | 89 | function getBdPluginsPath() { 90 | const dataPath = 91 | process.env.APPDATA || 92 | process.env.XDG_CONFIG_HOME || 93 | (process.platform === "darwin" 94 | ? `${process.env.HOME}/Library/Application Support` 95 | : `${process.env.HOME}/.config`); 96 | 97 | return path.join(dataPath, "BetterDiscord", "plugins"); 98 | } 99 | 100 | class BdHeader { 101 | constructor() { 102 | this.properties = new Map(); 103 | } 104 | 105 | set(name, value) { 106 | if (!value) return; 107 | this.properties.set(name, value); 108 | } 109 | 110 | remove(name) { 111 | this.properties.delete(name); 112 | } 113 | 114 | toString() { 115 | let result = "/**\n"; 116 | 117 | for (const [name, value] of this.properties) { 118 | result += ` * @${name} ${value}\n`; 119 | } 120 | 121 | result += " */"; 122 | 123 | return result; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /packages/BiggerStreamPreview/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { Webpack, ContextMenu } = BdApi; 4 | const { Filters } = Webpack; 5 | 6 | const openModal = Webpack.getModule(Filters.byStrings("modalKey", "Layer", "onCloseCallback"), { searchExports: true }); 7 | 8 | const ModalRoot = Webpack.getModule(Filters.byStrings("impressionType", "MODAL"), { searchExports: true }); 9 | const ImageModal = Webpack.getModule(Filters.byStrings( 10 | '"src","original","placeholder",' + 11 | '"width","height","animated",'+ 12 | '"children","responsive","renderLinkComponent",' + 13 | '"maxWidth","maxHeight","shouldAnimate"' 14 | ), { searchExports: true }); 15 | const ModalSize = Webpack.getModule(Filters.byProps("DYNAMIC"), { searchExports: true }); 16 | 17 | const MaskedLink = Webpack.getModule(m => m?.type?.toString()?.includes("MASKED_LINK"), { searchExports: true }); 18 | 19 | const useStateFromStores = Webpack.getModule(Filters.byStrings("useStateFromStores"), { searchExports: true }); 20 | 21 | const StreamStore = Webpack.getModule(Filters.byProps("getStreamForUser")); 22 | const StreamPreviewStore = Webpack.getModule(Filters.byProps("getPreviewURL")); 23 | 24 | const imageModalStyles = Webpack.getModule(Filters.byProps("modal", "image")); 25 | 26 | export default class BiggerStreamPreview { 27 | start() { 28 | this.patchUserContextMenu(); 29 | this.patchStreamContextMenu(); 30 | } 31 | 32 | stop() { 33 | this.unpatchUserContextMenu(); 34 | this.unpatchStreamContextMenu(); 35 | } 36 | 37 | patchUserContextMenu() { 38 | ContextMenu.patch("user-context", this.handleUserContextMenu); 39 | } 40 | 41 | unpatchUserContextMenu() { 42 | ContextMenu.unpatch("user-context", this.handleUserContextMenu); 43 | } 44 | 45 | patchStreamContextMenu() { 46 | ContextMenu.patch("stream-context", this.handleStreamContextMenu); 47 | } 48 | 49 | unpatchStreamContextMenu() { 50 | ContextMenu.unpatch("stream-context", this.handleStreamContextMenu); 51 | } 52 | 53 | appendStreamPreviewMenuGroup(menu, previewUrl) { 54 | menu.props.children.splice( 55 | menu.props.children.length - 1, 56 | 0, 57 | this.buildStreamPreviewMenuGroup(previewUrl) 58 | ); 59 | } 60 | 61 | buildStreamPreviewMenuGroup(previewUrl) { 62 | return ( 63 | 64 | this.openImageModal(previewUrl)} 68 | disabled={!previewUrl} 69 | /> 70 | 71 | ); 72 | } 73 | 74 | openImageModal(previewUrl) { 75 | openModal(props => ( 76 | 77 | } 84 | /> 85 | 86 | )); 87 | } 88 | 89 | handleUserContextMenu = (menu, { user }) => { 90 | const [stream, previewUrl] = useStateFromStores([StreamStore, StreamPreviewStore], () => { 91 | const stream = StreamStore.getAnyStreamForUser(user.id); 92 | const previewUrl = stream && StreamPreviewStore.getPreviewURL( 93 | stream.guildId, 94 | stream.channelId, 95 | stream.ownerId 96 | ); 97 | 98 | return [stream, previewUrl]; 99 | }); 100 | 101 | if (stream) { 102 | this.appendStreamPreviewMenuGroup(menu, previewUrl); 103 | } 104 | }; 105 | 106 | handleStreamContextMenu = (menu, { stream }) => { 107 | const previewUrl = useStateFromStores([StreamPreviewStore], () => StreamPreviewStore.getPreviewURL( 108 | stream.guildId, 109 | stream.channelId, 110 | stream.ownerId 111 | )); 112 | 113 | this.appendStreamPreviewMenuGroup(menu.props.children, previewUrl); 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /packages/WhoReacted/src/components/Reactors.jsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | 3 | import { Reactor, MaskedReactor } from './Reactor'; 4 | 5 | import { BoundedBdApi } from '../environment'; 6 | 7 | import { useStateFromStores } from '../hooks/useStateFromStores'; 8 | import { useSettings } from '../stores/SettingsStore'; 9 | 10 | const { Webpack } = BoundedBdApi; 11 | const { Filters } = Webpack; 12 | 13 | const ReactionStore = Webpack.getModule(Filters.byProps('getReactions')); 14 | const ChannelStore = Webpack.getModule(Filters.byProps('getChannel', 'hasChannel')); 15 | const UserStore = Webpack.getModule(Filters.byProps('getUser', 'getCurrentUser')); 16 | const RelationshipStore = Webpack.getModule(Filters.byProps('isBlocked')); 17 | 18 | export function Reactors({ count, channel, users, max, size, overlap, spacing }) { 19 | const usersShown = Math.min(max, users.length); 20 | 21 | const hasMoreUsers = count > usersShown; 22 | const userSummary = users.slice(0, usersShown); 23 | 24 | if (userSummary.length == 0) { 25 | return null; 26 | } 27 | 28 | return ( 29 |
30 | {userSummary.map((user, index) => index == count - 1 31 | ? ( 32 | 37 | ) 38 | : ( 39 | 46 | ) 47 | )} 48 | 49 | {hasMoreUsers && ( 50 |
60 | +{count - usersShown} 61 |
62 | )} 63 |
64 | ); 65 | } 66 | 67 | export function SmartReactors({ message, emoji, count, type }) { 68 | const [settings] = useSettings(); 69 | const ReactorsComponent = useMemo(() => { 70 | let component = Reactors; 71 | 72 | if (settings.hideSelf) { 73 | component = withSelfHidden(component); 74 | } 75 | 76 | if (settings.hideBots) { 77 | component = withBotsHidden(component); 78 | } 79 | 80 | if (settings.hideBlocked) { 81 | component = withBlockedHidden(component); 82 | } 83 | 84 | return withStoresConnected(component); 85 | }, [settings.hideSelf, settings.hideBots, settings.hideBlocked]); 86 | 87 | function shouldHide() { 88 | return isEmojiAboveThreshold() && 89 | isTotalReactionsAboveThreshold() && 90 | isReactionsPerEmojiAboveThreshold(); 91 | } 92 | 93 | function isEmojiAboveThreshold() { 94 | if (isThresholdDisabled(settings.emojiThreshold)) { 95 | return false; 96 | } 97 | 98 | return message.reactions.length > settings.emojiThreshold; 99 | } 100 | 101 | function isTotalReactionsAboveThreshold() { 102 | if (isThresholdDisabled(settings.reactionsTotalThreshold)) { 103 | return false; 104 | } 105 | 106 | return message.reactions.reduce((sum, reaction) => sum + reaction.count, 0) > settings.reactionsTotalThreshold; 107 | } 108 | 109 | function isReactionsPerEmojiAboveThreshold() { 110 | if (!isThresholdDisabled(settings.reactionsPerEmojiThreshold)) { 111 | for (const reaction of message.reactions) { 112 | if (reaction.count > settings.reactionsPerEmojiThreshold) { 113 | return true; 114 | } 115 | } 116 | } 117 | 118 | return false; 119 | } 120 | 121 | function isThresholdDisabled(threshold) { 122 | return threshold == 0; 123 | } 124 | 125 | if (shouldHide()) { 126 | return null; 127 | } 128 | 129 | return ( 130 | 140 | ); 141 | } 142 | 143 | export function withStoresConnected(ReactorsComponent) { 144 | return props => { 145 | const channel = useStateFromStores([ChannelStore], () => ChannelStore.getChannel(props.message.getChannelId())); 146 | const users = useStateFromStores( 147 | [ReactionStore], 148 | () => Object.values(ReactionStore.getReactions( 149 | props.message.getChannelId(), 150 | props.message.id, 151 | props.emoji, 152 | 100, 153 | props.type 154 | )) 155 | ); 156 | 157 | return ( 158 | 159 | ); 160 | }; 161 | } 162 | 163 | export function withSelfHidden(ReactorsComponent) { 164 | return props => { 165 | const currentUser = useStateFromStores([UserStore], () => UserStore.getCurrentUser()); 166 | const filteredUsers = props.users.filter(user => user.id != currentUser.id) 167 | 168 | return ( 169 | 173 | ); 174 | }; 175 | } 176 | 177 | export function withBotsHidden(ReactorsComponent) { 178 | return props => { 179 | const filteredUsers = props.users.filter(user => !user.bot); 180 | 181 | return ( 182 | 186 | ); 187 | }; 188 | } 189 | 190 | export function withBlockedHidden(ReactorsComponent) { 191 | return props => { 192 | const filteredUsers = useStateFromStores( 193 | [RelationshipStore], 194 | () => props.users.filter(user => !RelationshipStore.isBlocked(user.id)) 195 | ); 196 | 197 | return ( 198 | 202 | ); 203 | }; 204 | } 205 | -------------------------------------------------------------------------------- /packages/WhoReacted/src/components/SettingsPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { BoundedBdApi } from '../environment'; 4 | import { useSettings } from '../stores/SettingsStore'; 5 | 6 | const { Webpack } = BdApi; 7 | const { Filters } = Webpack; 8 | 9 | const margins = BdApi.findModuleByProps('marginLarge'); 10 | 11 | const FormSection = Webpack.getModule(Filters.byStrings('.titleClassName', '.sectionTitle'), { searchExports: true }); 12 | const FormItem = Webpack.getModule(m => Filters.byStrings('.titleClassName', '.required')(m?.render), { searchExports: true }); 13 | const FormTitle = Webpack.getModule(Filters.byStrings('.faded', '.required'), { searchExports: true }); 14 | const FormText = Webpack.getModule(m => m?.Types?.INPUT_PLACEHOLDER, { searchExports: true }); 15 | const TextInput = Webpack.getModule(m => m?.defaultProps?.type === 'text', { searchExports: true }); 16 | const Slider = Webpack.getModule(Filters.byStrings('.asValueChanges'), { searchExports: true }); 17 | const SwitchItem = Webpack.getModule(Filters.byStrings('.tooltipNote'), { searchExports: true }); 18 | 19 | export function SettingsPanel() { 20 | const [ 21 | settings, 22 | defaults, 23 | update 24 | ] = useSettings(); 25 | 26 | function handlePixelMarkerRender(value) { 27 | return `${value}px`; 28 | } 29 | 30 | function handlePercentageMarkerRender(value) { 31 | return `${value.toFixed(2)}%`; 32 | } 33 | 34 | function handleThresholdMarkerRender(value) { 35 | if (value == 0) { 36 | return 'Off'; 37 | } 38 | 39 | if (value >= 1000) { 40 | return `${value / 1000}k`; 41 | } 42 | 43 | return value; 44 | } 45 | 46 | return <> 47 | 48 | Appearance 49 | 50 | 51 | Maximum Avatars 52 | { 57 | const number = parseInt(value); 58 | 59 | if (number >= 1 && number <= 100) { 60 | update('max', number); 61 | } else { 62 | BoundedBdApi.showToast('The value must be a number from 1 to 100.', 'danger'); 63 | } 64 | }} 65 | /> 66 | 67 | Sets the maximum number of avatars shown per emoji from 1 to 68 | 100. 69 | 70 | 71 | 72 | 73 | Avatar Size 74 | update('avatarSize', value)} 89 | /> 90 | 91 | Sets the size of the avatars. 92 | 93 | 94 | 95 | 96 | Avatar Overlap 97 | update('avatarOverlap', value)} 110 | /> 111 | 112 | Sets how much an avatar covers the previous one. 113 | 114 | 115 | 116 | 117 | Avatar Spacing 118 | update('avatarSpacing', value)} 134 | /> 135 | 136 | Sets the gap between two avatars. 137 | 138 | 139 | 140 | 141 | 142 | Thresholds 143 | 144 | 145 | Emoji Threshold 146 | update('emojiThreshold', value)} 175 | /> 176 | 177 | Hides the reactors when the number of emojis exceeds the 178 | threshold. 179 | 180 | 181 | 182 | 183 | Reactions Total Threshold 184 | update('reactionsTotalThreshold', value)} 205 | /> 206 | 207 | Hides the reactors when the sum of the number of reactions 208 | in all emojis exceeds the threshold. 209 | 210 | 211 | 212 | 213 | Reactions per Emoji Threshold 214 | update('reactionsPerEmojiThreshold', value)} 231 | /> 232 | 233 | Hides the reactors when the number of reactions on a single 234 | emoji exceeds the threshold. 235 | 236 | 237 | 238 | 239 | 240 | Filters 241 | 242 | update('hideSelf', checked)} 245 | > 246 | Hide Self 247 | 248 | 249 | update('hideBots', checked)} 252 | > 253 | Hide Bots 254 | 255 | 256 | update('hideBlocked', checked)} 259 | > 260 | Hide Blocked Users 261 | 262 | 263 | ; 264 | } 265 | --------------------------------------------------------------------------------